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