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