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