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