2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.documentsui;
19 import static com.android.documentsui.DocumentsActivity.TAG;
20 import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
21 import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
22 import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
23 import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
24 import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
25 import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
26 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
27 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
28 import static com.android.documentsui.model.DocumentInfo.getCursorString;
30 import android.app.ActivityManager;
31 import android.app.Fragment;
32 import android.app.FragmentManager;
33 import android.app.FragmentTransaction;
34 import android.app.LoaderManager.LoaderCallbacks;
35 import android.content.ContentProviderClient;
36 import android.content.ContentResolver;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.Loader;
41 import android.database.Cursor;
42 import android.graphics.Bitmap;
43 import android.graphics.Point;
44 import android.graphics.drawable.Drawable;
45 import android.graphics.drawable.InsetDrawable;
46 import android.net.Uri;
47 import android.os.AsyncTask;
48 import android.os.Bundle;
49 import android.os.CancellationSignal;
50 import android.os.Parcelable;
51 import android.provider.DocumentsContract;
52 import android.provider.DocumentsContract.Document;
53 import android.text.format.DateUtils;
54 import android.text.format.Formatter;
55 import android.text.format.Time;
56 import android.util.Log;
57 import android.util.SparseArray;
58 import android.util.SparseBooleanArray;
59 import android.view.ActionMode;
60 import android.view.LayoutInflater;
61 import android.view.Menu;
62 import android.view.MenuItem;
63 import android.view.View;
64 import android.view.ViewGroup;
65 import android.widget.AbsListView;
66 import android.widget.AbsListView.MultiChoiceModeListener;
67 import android.widget.AbsListView.RecyclerListener;
68 import android.widget.AdapterView;
69 import android.widget.AdapterView.OnItemClickListener;
70 import android.widget.BaseAdapter;
71 import android.widget.FrameLayout;
72 import android.widget.GridView;
73 import android.widget.ImageView;
74 import android.widget.ListView;
75 import android.widget.TextView;
76 import android.widget.Toast;
78 import com.android.documentsui.DocumentsActivity.State;
79 import com.android.documentsui.RecentsProvider.StateColumns;
80 import com.android.documentsui.model.DocumentInfo;
81 import com.android.documentsui.model.RootInfo;
82 import com.google.android.collect.Lists;
84 import java.util.ArrayList;
85 import java.util.List;
86 import java.util.concurrent.atomic.AtomicInteger;
89 * Display the documents inside a single directory.
91 public class DirectoryFragment extends Fragment {
93 private View mEmptyView;
94 private ListView mListView;
95 private GridView mGridView;
97 private AbsListView mCurrentView;
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;
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;
108 private int mType = TYPE_NORMAL;
109 private String mStateKey;
111 private int mLastMode = MODE_UNKNOWN;
112 private int mLastSortOrder = SORT_ORDER_UNKNOWN;
113 private boolean mLastShowSize = false;
115 private boolean mHideGridTitles = false;
117 private boolean mSvelteRecents;
118 private Point mThumbSize;
120 private DocumentsAdapter mAdapter;
121 private LoaderCallbacks<DirectoryResult> mCallbacks;
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";
129 private static AtomicInteger sLoaderId = new AtomicInteger(4000);
131 private final int mLoaderId = sLoaderId.incrementAndGet();
133 public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
134 show(fm, TYPE_NORMAL, root, doc, null, anim);
137 public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
138 show(fm, TYPE_SEARCH, root, null, query, anim);
141 public static void showRecentsOpen(FragmentManager fm, int anim) {
142 show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
145 private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
146 String query, int anim) {
147 final Bundle args = new Bundle();
148 args.putInt(EXTRA_TYPE, type);
149 args.putParcelable(EXTRA_ROOT, root);
150 args.putParcelable(EXTRA_DOC, doc);
151 args.putString(EXTRA_QUERY, query);
153 final FragmentTransaction ft = fm.beginTransaction();
156 args.putBoolean(EXTRA_IGNORE_STATE, true);
159 args.putBoolean(EXTRA_IGNORE_STATE, true);
160 ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen);
163 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up);
167 final DirectoryFragment fragment = new DirectoryFragment();
168 fragment.setArguments(args);
170 ft.replace(R.id.container_directory, fragment);
171 ft.commitAllowingStateLoss();
174 private static String buildStateKey(RootInfo root, DocumentInfo doc) {
175 final StringBuilder builder = new StringBuilder();
176 builder.append(root != null ? root.authority : "null").append(';');
177 builder.append(root != null ? root.rootId : "null").append(';');
178 builder.append(doc != null ? doc.documentId : "null");
179 return builder.toString();
182 public static DirectoryFragment get(FragmentManager fm) {
183 // TODO: deal with multiple directories shown at once
184 return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
188 public View onCreateView(
189 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
190 final Context context = inflater.getContext();
191 final View view = inflater.inflate(R.layout.fragment_directory, container, false);
193 mEmptyView = view.findViewById(android.R.id.empty);
195 mListView = (ListView) view.findViewById(R.id.list);
196 mListView.setOnItemClickListener(mItemListener);
197 mListView.setMultiChoiceModeListener(mMultiListener);
198 mListView.setRecyclerListener(mRecycleListener);
200 mGridView = (GridView) view.findViewById(R.id.grid);
201 mGridView.setOnItemClickListener(mItemListener);
202 mGridView.setMultiChoiceModeListener(mMultiListener);
203 mGridView.setRecyclerListener(mRecycleListener);
209 public void onDestroyView() {
210 super.onDestroyView();
212 // Cancel any outstanding thumbnail requests
213 final ViewGroup target = (mListView.getAdapter() != null) ? mListView : mGridView;
214 final int count = target.getChildCount();
215 for (int i = 0; i < count; i++) {
216 final View view = target.getChildAt(i);
217 mRecycleListener.onMovedToScrapHeap(view);
222 public void onActivityCreated(Bundle savedInstanceState) {
223 super.onActivityCreated(savedInstanceState);
225 final Context context = getActivity();
226 final State state = getDisplayState(DirectoryFragment.this);
228 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
229 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
231 mAdapter = new DocumentsAdapter();
232 mType = getArguments().getInt(EXTRA_TYPE);
233 mStateKey = buildStateKey(root, doc);
235 if (mType == TYPE_RECENT_OPEN) {
236 // Hide titles when showing recents for picking images/videos
237 mHideGridTitles = MimePredicate.mimeMatches(
238 MimePredicate.VISUAL_MIMES, state.acceptMimes);
240 mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
243 final ActivityManager am = (ActivityManager) context.getSystemService(
244 Context.ACTIVITY_SERVICE);
245 mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
247 mCallbacks = new LoaderCallbacks<DirectoryResult>() {
249 public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
250 final String query = getArguments().getString(EXTRA_QUERY);
255 contentsUri = DocumentsContract.buildChildDocumentsUri(
256 doc.authority, doc.documentId);
257 if (state.action == ACTION_MANAGE) {
258 contentsUri = DocumentsContract.setManageMode(contentsUri);
260 return new DirectoryLoader(
261 context, mType, root, doc, contentsUri, state.userSortOrder);
263 contentsUri = DocumentsContract.buildSearchDocumentsUri(
264 root.authority, root.rootId, query);
265 if (state.action == ACTION_MANAGE) {
266 contentsUri = DocumentsContract.setManageMode(contentsUri);
268 return new DirectoryLoader(
269 context, mType, root, doc, contentsUri, state.userSortOrder);
270 case TYPE_RECENT_OPEN:
271 final RootsCache roots = DocumentsApplication.getRootsCache(context);
272 return new RecentLoader(context, roots, state);
274 throw new IllegalStateException("Unknown type " + mType);
279 public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
280 if (!isAdded()) return;
282 mAdapter.swapResult(result);
284 // Push latest state up to UI
285 // TODO: if mode change was racing with us, don't overwrite it
286 if (result.mode != MODE_UNKNOWN) {
287 state.derivedMode = result.mode;
289 state.derivedSortOrder = result.sortOrder;
290 ((DocumentsActivity) context).onStateChanged();
292 updateDisplayState();
294 // When launched into empty recents, show drawer
295 if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) {
296 ((DocumentsActivity) context).setRootsDrawerOpen(true);
299 // Restore any previous instance state
300 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
301 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
302 getView().restoreHierarchyState(container);
303 } else if (mLastSortOrder != state.derivedSortOrder) {
304 mListView.smoothScrollToPosition(0);
305 mGridView.smoothScrollToPosition(0);
308 mLastSortOrder = state.derivedSortOrder;
312 public void onLoaderReset(Loader<DirectoryResult> loader) {
313 mAdapter.swapResult(null);
317 // Kick off loader at least once
318 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
320 updateDisplayState();
324 public void onStop() {
327 // Remember last scroll location
328 final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
329 getView().saveHierarchyState(container);
330 final State state = getDisplayState(this);
331 state.dirState.put(mStateKey, container);
335 public void onResume() {
337 updateDisplayState();
340 public void onUserSortOrderChanged() {
341 // Sort order change always triggers reload; we'll trigger state change
343 getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
346 public void onUserModeChanged() {
347 final ContentResolver resolver = getActivity().getContentResolver();
348 final State state = getDisplayState(this);
350 final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
351 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
353 if (root != null && doc != null) {
354 final Uri stateUri = RecentsProvider.buildState(
355 root.authority, root.rootId, doc.documentId);
356 final ContentValues values = new ContentValues();
357 values.put(StateColumns.MODE, state.userMode);
359 new AsyncTask<Void, Void, Void>() {
361 protected Void doInBackground(Void... params) {
362 resolver.insert(stateUri, values);
368 // Mode change is just visual change; no need to kick loader, and
369 // deliver change event immediately.
370 state.derivedMode = state.userMode;
371 ((DocumentsActivity) getActivity()).onStateChanged();
373 updateDisplayState();
376 private void updateDisplayState() {
377 final State state = getDisplayState(this);
379 if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return;
380 mLastMode = state.derivedMode;
381 mLastShowSize = state.showSize;
383 mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE);
384 mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE);
386 final int choiceMode;
387 if (state.allowMultiple) {
388 choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
390 choiceMode = ListView.CHOICE_MODE_NONE;
394 if (state.derivedMode == MODE_GRID) {
395 thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
396 mListView.setAdapter(null);
397 mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
398 mGridView.setAdapter(mAdapter);
399 mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
400 mGridView.setNumColumns(GridView.AUTO_FIT);
401 mGridView.setChoiceMode(choiceMode);
402 mCurrentView = mGridView;
403 } else if (state.derivedMode == MODE_LIST) {
404 thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
405 mGridView.setAdapter(null);
406 mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
407 mListView.setAdapter(mAdapter);
408 mListView.setChoiceMode(choiceMode);
409 mCurrentView = mListView;
411 throw new IllegalStateException("Unknown state " + state.derivedMode);
414 mThumbSize = new Point(thumbSize, thumbSize);
417 private OnItemClickListener mItemListener = new OnItemClickListener() {
419 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
420 final Cursor cursor = mAdapter.getItem(position);
421 if (cursor != null) {
422 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
423 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
424 if (isDocumentEnabled(docMimeType, docFlags)) {
425 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
426 ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
432 private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
434 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
435 mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
440 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
441 final State state = getDisplayState(DirectoryFragment.this);
443 final MenuItem open = menu.findItem(R.id.menu_open);
444 final MenuItem share = menu.findItem(R.id.menu_share);
445 final MenuItem delete = menu.findItem(R.id.menu_delete);
447 final boolean manageMode = state.action == ACTION_MANAGE;
448 open.setVisible(!manageMode);
449 share.setVisible(manageMode);
450 delete.setVisible(manageMode);
456 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
457 final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
458 final ArrayList<DocumentInfo> docs = Lists.newArrayList();
459 final int size = checked.size();
460 for (int i = 0; i < size; i++) {
461 if (checked.valueAt(i)) {
462 final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
463 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
468 final int id = item.getItemId();
469 if (id == R.id.menu_open) {
470 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
474 } else if (id == R.id.menu_share) {
475 onShareDocuments(docs);
479 } else if (id == R.id.menu_delete) {
480 onDeleteDocuments(docs);
490 public void onDestroyActionMode(ActionMode mode) {
495 public void onItemCheckedStateChanged(
496 ActionMode mode, int position, long id, boolean checked) {
498 // Directories and footer items cannot be checked
499 boolean valid = false;
501 final Cursor cursor = mAdapter.getItem(position);
502 if (cursor != null) {
503 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
504 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
505 if (!Document.MIME_TYPE_DIR.equals(docMimeType)) {
506 valid = isDocumentEnabled(docMimeType, docFlags);
511 mCurrentView.setItemChecked(position, false);
515 mode.setTitle(getResources()
516 .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
520 private RecyclerListener mRecycleListener = new RecyclerListener() {
522 public void onMovedToScrapHeap(View view) {
523 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
524 if (iconThumb != null) {
525 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
526 if (oldTask != null) {
527 oldTask.reallyCancel();
528 iconThumb.setTag(null);
534 private void onShareDocuments(List<DocumentInfo> docs) {
536 if (docs.size() == 1) {
537 final DocumentInfo doc = docs.get(0);
539 intent = new Intent(Intent.ACTION_SEND);
540 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
541 intent.addCategory(Intent.CATEGORY_DEFAULT);
542 intent.setType(doc.mimeType);
543 intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
545 } else if (docs.size() > 1) {
546 intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
547 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
548 intent.addCategory(Intent.CATEGORY_DEFAULT);
550 final ArrayList<String> mimeTypes = Lists.newArrayList();
551 final ArrayList<Uri> uris = Lists.newArrayList();
552 for (DocumentInfo doc : docs) {
553 mimeTypes.add(doc.mimeType);
554 uris.add(doc.derivedUri);
557 intent.setType(findCommonMimeType(mimeTypes));
558 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
564 intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
565 startActivity(intent);
568 private void onDeleteDocuments(List<DocumentInfo> docs) {
569 final Context context = getActivity();
570 final ContentResolver resolver = context.getContentResolver();
572 boolean hadTrouble = false;
573 for (DocumentInfo doc : docs) {
574 if (!doc.isDeleteSupported()) {
575 Log.w(TAG, "Skipping " + doc);
580 ContentProviderClient client = null;
582 client = DocumentsApplication.acquireUnstableProviderOrThrow(
583 resolver, doc.derivedUri.getAuthority());
584 DocumentsContract.deleteDocument(client, doc.derivedUri);
585 } catch (Exception e) {
586 Log.w(TAG, "Failed to delete " + doc);
589 ContentProviderClient.releaseQuietly(client);
594 Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
598 private static State getDisplayState(Fragment fragment) {
599 return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
602 private static abstract class Footer {
603 private final int mItemViewType;
605 public Footer(int itemViewType) {
606 mItemViewType = itemViewType;
609 public abstract View getView(View convertView, ViewGroup parent);
611 public int getItemViewType() {
612 return mItemViewType;
616 private class LoadingFooter extends Footer {
617 public LoadingFooter() {
622 public View getView(View convertView, ViewGroup parent) {
623 final Context context = parent.getContext();
624 final State state = getDisplayState(DirectoryFragment.this);
626 if (convertView == null) {
627 final LayoutInflater inflater = LayoutInflater.from(context);
628 if (state.derivedMode == MODE_LIST) {
629 convertView = inflater.inflate(R.layout.item_loading_list, parent, false);
630 } else if (state.derivedMode == MODE_GRID) {
631 convertView = inflater.inflate(R.layout.item_loading_grid, parent, false);
633 throw new IllegalStateException();
641 private class MessageFooter extends Footer {
642 private final int mIcon;
643 private final String mMessage;
645 public MessageFooter(int itemViewType, int icon, String message) {
652 public View getView(View convertView, ViewGroup parent) {
653 final Context context = parent.getContext();
654 final State state = getDisplayState(DirectoryFragment.this);
656 if (convertView == null) {
657 final LayoutInflater inflater = LayoutInflater.from(context);
658 if (state.derivedMode == MODE_LIST) {
659 convertView = inflater.inflate(R.layout.item_message_list, parent, false);
660 } else if (state.derivedMode == MODE_GRID) {
661 convertView = inflater.inflate(R.layout.item_message_grid, parent, false);
663 throw new IllegalStateException();
667 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
668 final TextView title = (TextView) convertView.findViewById(android.R.id.title);
669 icon.setImageResource(mIcon);
670 title.setText(mMessage);
675 private class DocumentsAdapter extends BaseAdapter {
676 private Cursor mCursor;
677 private int mCursorCount;
679 private List<Footer> mFooters = Lists.newArrayList();
681 public void swapResult(DirectoryResult result) {
682 mCursor = result != null ? result.cursor : null;
683 mCursorCount = mCursor != null ? mCursor.getCount() : 0;
687 final Bundle extras = mCursor != null ? mCursor.getExtras() : null;
688 if (extras != null) {
689 final String info = extras.getString(DocumentsContract.EXTRA_INFO);
691 mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info));
693 final String error = extras.getString(DocumentsContract.EXTRA_ERROR);
695 mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error));
697 if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) {
698 mFooters.add(new LoadingFooter());
702 if (result != null && result.exception != null) {
703 mFooters.add(new MessageFooter(
704 3, R.drawable.ic_dialog_alert, getString(R.string.query_error)));
708 mEmptyView.setVisibility(View.VISIBLE);
710 mEmptyView.setVisibility(View.GONE);
713 notifyDataSetChanged();
717 public View getView(int position, View convertView, ViewGroup parent) {
718 if (position < mCursorCount) {
719 return getDocumentView(position, convertView, parent);
721 position -= mCursorCount;
722 convertView = mFooters.get(position).getView(convertView, parent);
723 // Only the view itself is disabled; contents inside shouldn't
725 convertView.setEnabled(false);
730 private View getDocumentView(int position, View convertView, ViewGroup parent) {
731 final Context context = parent.getContext();
732 final State state = getDisplayState(DirectoryFragment.this);
734 final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
736 final RootsCache roots = DocumentsApplication.getRootsCache(context);
737 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
738 context, mThumbSize);
740 if (convertView == null) {
741 final LayoutInflater inflater = LayoutInflater.from(context);
742 if (state.derivedMode == MODE_LIST) {
743 convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
744 } else if (state.derivedMode == MODE_GRID) {
745 convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
747 // Apply padding to grid items
748 final FrameLayout grid = (FrameLayout) convertView;
749 final int gridPadding = getResources()
750 .getDimensionPixelSize(R.dimen.grid_padding);
752 // Tricksy hobbitses! We need to fully clear the drawable so
753 // the view doesn't clobber the new InsetDrawable callback
754 // when setting back later.
755 final Drawable fg = grid.getForeground();
756 final Drawable bg = grid.getBackground();
757 grid.setForeground(null);
758 grid.setBackground(null);
759 grid.setForeground(new InsetDrawable(fg, gridPadding));
760 grid.setBackground(new InsetDrawable(bg, gridPadding));
762 throw new IllegalStateException();
766 final Cursor cursor = getItem(position);
768 final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
769 final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
770 final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
771 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
772 final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
773 final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
774 final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
775 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
776 final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
777 final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
779 final View line1 = convertView.findViewById(R.id.line1);
780 final View line2 = convertView.findViewById(R.id.line2);
782 final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime);
783 final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb);
784 final TextView title = (TextView) convertView.findViewById(android.R.id.title);
785 final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
786 final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2);
787 final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
788 final TextView date = (TextView) convertView.findViewById(R.id.date);
789 final TextView size = (TextView) convertView.findViewById(R.id.size);
791 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
792 if (oldTask != null) {
793 oldTask.reallyCancel();
794 iconThumb.setTag(null);
797 iconMime.animate().cancel();
798 iconThumb.animate().cancel();
800 final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
801 final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
802 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
803 final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
805 boolean cacheHit = false;
807 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
808 final Bitmap cachedResult = thumbs.get(uri);
809 if (cachedResult != null) {
810 iconThumb.setImageBitmap(cachedResult);
813 iconThumb.setImageDrawable(null);
814 final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
815 uri, iconMime, iconThumb, mThumbSize);
816 iconThumb.setTag(task);
817 task.executeOnExecutor(ProviderExecutor.forAuthority(docAuthority));
821 // Always throw MIME icon into place, even when a thumbnail is being
822 // loaded in background.
824 iconMime.setAlpha(0f);
825 iconMime.setImageDrawable(null);
826 iconThumb.setAlpha(1f);
828 iconMime.setAlpha(1f);
829 iconThumb.setAlpha(0f);
830 iconThumb.setImageDrawable(null);
832 iconMime.setImageDrawable(
833 IconUtils.loadPackageIcon(context, docAuthority, docIcon));
835 iconMime.setImageDrawable(IconUtils.loadMimeIcon(
836 context, docMimeType, docAuthority, docId, state.derivedMode));
840 boolean hasLine1 = false;
841 boolean hasLine2 = false;
843 final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles;
845 title.setText(docDisplayName);
849 Drawable iconDrawable = null;
850 if (mType == TYPE_RECENT_OPEN) {
851 // We've already had to enumerate roots before any results can
852 // be shown, so this will never block.
853 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
854 iconDrawable = root.loadIcon(context);
856 if (summary != null) {
857 final boolean alwaysShowSummary = getResources()
858 .getBoolean(R.bool.always_show_summary);
859 if (alwaysShowSummary) {
860 summary.setText(root.getDirectoryString());
861 summary.setVisibility(View.VISIBLE);
864 if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
865 // No summary needed if icon speaks for itself
866 summary.setVisibility(View.INVISIBLE);
868 summary.setText(root.getDirectoryString());
869 summary.setVisibility(View.VISIBLE);
870 summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
876 // Directories showing thumbnails in grid mode get a little icon
877 // hint to remind user they're a directory.
878 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
880 iconDrawable = context.getResources().getDrawable(R.drawable.ic_root_folder);
883 if (summary != null) {
884 if (docSummary != null) {
885 summary.setText(docSummary);
886 summary.setVisibility(View.VISIBLE);
889 summary.setVisibility(View.INVISIBLE);
894 if (icon1 != null) icon1.setVisibility(View.GONE);
895 if (icon2 != null) icon2.setVisibility(View.GONE);
897 if (iconDrawable != null) {
899 icon1.setVisibility(View.VISIBLE);
900 icon1.setImageDrawable(iconDrawable);
902 icon2.setVisibility(View.VISIBLE);
903 icon2.setImageDrawable(iconDrawable);
907 if (docLastModified == -1) {
910 date.setText(formatTime(context, docLastModified));
914 if (state.showSize) {
915 size.setVisibility(View.VISIBLE);
916 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
919 size.setText(Formatter.formatFileSize(context, docSize));
923 size.setVisibility(View.GONE);
927 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE);
930 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE);
933 final boolean enabled = isDocumentEnabled(docMimeType, docFlags);
935 setEnabledRecursive(convertView, true);
936 iconMime.setAlpha(1f);
937 iconThumb.setAlpha(1f);
938 if (icon1 != null) icon1.setAlpha(1f);
939 if (icon2 != null) icon2.setAlpha(1f);
941 setEnabledRecursive(convertView, false);
942 iconMime.setAlpha(0.5f);
943 iconThumb.setAlpha(0.5f);
944 if (icon1 != null) icon1.setAlpha(0.5f);
945 if (icon2 != null) icon2.setAlpha(0.5f);
952 public int getCount() {
953 return mCursorCount + mFooters.size();
957 public Cursor getItem(int position) {
958 if (position < mCursorCount) {
959 mCursor.moveToPosition(position);
967 public long getItemId(int position) {
972 public int getViewTypeCount() {
977 public int getItemViewType(int position) {
978 if (position < mCursorCount) {
981 position -= mCursorCount;
982 return mFooters.get(position).getItemViewType();
987 private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> {
988 private final Uri mUri;
989 private final ImageView mIconMime;
990 private final ImageView mIconThumb;
991 private final Point mThumbSize;
992 private final CancellationSignal mSignal;
994 public ThumbnailAsyncTask(
995 Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize) {
997 mIconMime = iconMime;
998 mIconThumb = iconThumb;
999 mThumbSize = thumbSize;
1000 mSignal = new CancellationSignal();
1003 public void reallyCancel() {
1009 protected Bitmap doInBackground(Uri... params) {
1010 if (isCancelled()) return null;
1012 final Context context = mIconThumb.getContext();
1013 final ContentResolver resolver = context.getContentResolver();
1015 ContentProviderClient client = null;
1016 Bitmap result = null;
1018 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1019 resolver, mUri.getAuthority());
1020 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
1021 if (result != null) {
1022 final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1023 context, mThumbSize);
1024 thumbs.put(mUri, result);
1026 } catch (Exception e) {
1027 Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
1029 ContentProviderClient.releaseQuietly(client);
1035 protected void onPostExecute(Bitmap result) {
1036 if (mIconThumb.getTag() == this && result != null) {
1037 mIconThumb.setTag(null);
1038 mIconThumb.setImageBitmap(result);
1040 final float targetAlpha = mIconMime.isEnabled() ? 1f : 0.5f;
1041 mIconMime.setAlpha(targetAlpha);
1042 mIconMime.animate().alpha(0f).start();
1043 mIconThumb.setAlpha(0f);
1044 mIconThumb.animate().alpha(targetAlpha).start();
1049 private static String formatTime(Context context, long when) {
1050 // TODO: DateUtils should make this easier
1051 Time then = new Time();
1053 Time now = new Time();
1056 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
1057 | DateUtils.FORMAT_ABBREV_ALL;
1059 if (then.year != now.year) {
1060 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
1061 } else if (then.yearDay != now.yearDay) {
1062 flags |= DateUtils.FORMAT_SHOW_DATE;
1064 flags |= DateUtils.FORMAT_SHOW_TIME;
1067 return DateUtils.formatDateTime(context, when, flags);
1070 private String findCommonMimeType(List<String> mimeTypes) {
1071 String[] commonType = mimeTypes.get(0).split("/");
1072 if (commonType.length != 2) {
1076 for (int i = 1; i < mimeTypes.size(); i++) {
1077 String[] type = mimeTypes.get(i).split("/");
1078 if (type.length != 2) continue;
1080 if (!commonType[1].equals(type[1])) {
1081 commonType[1] = "*";
1084 if (!commonType[0].equals(type[0])) {
1085 commonType[0] = "*";
1086 commonType[1] = "*";
1091 return commonType[0] + "/" + commonType[1];
1094 private void setEnabledRecursive(View v, boolean enabled) {
1095 if (v == null) return;
1096 if (v.isEnabled() == enabled) return;
1097 v.setEnabled(enabled);
1099 if (v instanceof ViewGroup) {
1100 final ViewGroup vg = (ViewGroup) v;
1101 for (int i = vg.getChildCount() - 1; i >= 0; i--) {
1102 setEnabledRecursive(vg.getChildAt(i), enabled);
1107 private boolean isDocumentEnabled(String docMimeType, int docFlags) {
1108 final State state = getDisplayState(DirectoryFragment.this);
1110 // Directories are always enabled
1111 if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1115 // Read-only files are disabled when creating
1116 if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
1120 return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);