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