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