OSDN Git Service

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