DirectoryFragment.java revision a82c2e24bc226c123075331cdaafb171b3b07b85
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
17package com.android.documentsui;
18
19import static com.android.documentsui.DocumentsActivity.TAG;
20import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
21import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
22import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
23import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
24import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
25import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
26import static com.android.documentsui.model.DocumentInfo.getCursorInt;
27import static com.android.documentsui.model.DocumentInfo.getCursorLong;
28import static com.android.documentsui.model.DocumentInfo.getCursorString;
29
30import android.app.ActivityManager;
31import android.app.Fragment;
32import android.app.FragmentManager;
33import android.app.FragmentTransaction;
34import android.app.LoaderManager.LoaderCallbacks;
35import android.content.ContentProviderClient;
36import android.content.ContentResolver;
37import android.content.ContentValues;
38import android.content.Context;
39import android.content.Intent;
40import android.content.Loader;
41import android.database.Cursor;
42import android.graphics.Bitmap;
43import android.graphics.Point;
44import android.graphics.drawable.Drawable;
45import android.graphics.drawable.InsetDrawable;
46import android.net.Uri;
47import android.os.AsyncTask;
48import android.os.Bundle;
49import android.os.CancellationSignal;
50import android.os.Parcelable;
51import android.provider.DocumentsContract;
52import android.provider.DocumentsContract.Document;
53import android.text.format.DateUtils;
54import android.text.format.Formatter;
55import android.text.format.Time;
56import android.util.Log;
57import android.util.SparseArray;
58import android.util.SparseBooleanArray;
59import android.view.ActionMode;
60import android.view.LayoutInflater;
61import android.view.Menu;
62import android.view.MenuItem;
63import android.view.View;
64import android.view.ViewGroup;
65import android.widget.AbsListView;
66import android.widget.AbsListView.MultiChoiceModeListener;
67import android.widget.AbsListView.RecyclerListener;
68import android.widget.AdapterView;
69import android.widget.AdapterView.OnItemClickListener;
70import android.widget.BaseAdapter;
71import android.widget.FrameLayout;
72import android.widget.GridView;
73import android.widget.ImageView;
74import android.widget.ListView;
75import android.widget.TextView;
76import android.widget.Toast;
77
78import com.android.documentsui.DocumentsActivity.State;
79import com.android.documentsui.RecentsProvider.StateColumns;
80import com.android.documentsui.model.DocumentInfo;
81import com.android.documentsui.model.RootInfo;
82import com.google.android.collect.Lists;
83
84import java.util.ArrayList;
85import java.util.List;
86import java.util.concurrent.atomic.AtomicInteger;
87
88/**
89 * Display the documents inside a single directory.
90 */
91public 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}
1123