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