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