DirectoryFragment.java revision 251097b3789632000ccdaf7fb7d66a82ff37d882
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                        return new RecentLoader(context);
195                    default:
196                        throw new IllegalStateException("Unknown type " + mType);
197
198                }
199            }
200
201            @Override
202            public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
203                mAdapter.swapCursor(result.cursor);
204            }
205
206            @Override
207            public void onLoaderReset(Loader<DirectoryResult> loader) {
208                mAdapter.swapCursor(null);
209            }
210        };
211
212        updateDisplayState();
213    }
214
215    public void updateDisplayState() {
216        final State state = getDisplayState(this);
217
218        if (mLastSortOrder != state.sortOrder) {
219            getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
220            mLastSortOrder = state.sortOrder;
221        }
222
223        mListView.smoothScrollToPosition(0);
224        mGridView.smoothScrollToPosition(0);
225
226        mListView.setVisibility(state.mode == MODE_LIST ? View.VISIBLE : View.GONE);
227        mGridView.setVisibility(state.mode == MODE_GRID ? View.VISIBLE : View.GONE);
228
229        mFilter = new MimePredicate(state.acceptMimes);
230
231        final int choiceMode;
232        if (state.allowMultiple) {
233            choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
234        } else {
235            choiceMode = ListView.CHOICE_MODE_NONE;
236        }
237
238        final int thumbSize;
239        if (state.mode == MODE_GRID) {
240            thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
241            mListView.setAdapter(null);
242            mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
243            mGridView.setAdapter(mAdapter);
244            mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
245            mGridView.setNumColumns(GridView.AUTO_FIT);
246            mGridView.setChoiceMode(choiceMode);
247            mCurrentView = mGridView;
248        } else if (state.mode == MODE_LIST) {
249            thumbSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
250            mGridView.setAdapter(null);
251            mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
252            mListView.setAdapter(mAdapter);
253            mListView.setChoiceMode(choiceMode);
254            mCurrentView = mListView;
255        } else {
256            throw new IllegalStateException();
257        }
258
259        mThumbSize = new Point(thumbSize, thumbSize);
260    }
261
262    private OnItemClickListener mItemListener = new OnItemClickListener() {
263        @Override
264        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
265            final Cursor cursor = mAdapter.getItem(position);
266            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
267            if (mFilter.apply(doc)) {
268                ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
269            }
270        }
271    };
272
273    private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
274        @Override
275        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
276            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
277            return true;
278        }
279
280        @Override
281        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
282            final State state = getDisplayState(DirectoryFragment.this);
283
284            final MenuItem open = menu.findItem(R.id.menu_open);
285            final MenuItem share = menu.findItem(R.id.menu_share);
286            final MenuItem delete = menu.findItem(R.id.menu_delete);
287
288            final boolean manageMode = state.action == ACTION_MANAGE;
289            open.setVisible(!manageMode);
290            share.setVisible(manageMode);
291            delete.setVisible(manageMode);
292
293            return true;
294        }
295
296        @Override
297        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
298            final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
299            final ArrayList<DocumentInfo> docs = Lists.newArrayList();
300            final int size = checked.size();
301            for (int i = 0; i < size; i++) {
302                if (checked.valueAt(i)) {
303                    final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
304                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
305                    docs.add(doc);
306                }
307            }
308
309            final int id = item.getItemId();
310            if (id == R.id.menu_open) {
311                DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
312                mode.finish();
313                return true;
314
315            } else if (id == R.id.menu_share) {
316                onShareDocuments(docs);
317                mode.finish();
318                return true;
319
320            } else if (id == R.id.menu_delete) {
321                onDeleteDocuments(docs);
322                mode.finish();
323                return true;
324
325            } else {
326                return false;
327            }
328        }
329
330        @Override
331        public void onDestroyActionMode(ActionMode mode) {
332            // ignored
333        }
334
335        @Override
336        public void onItemCheckedStateChanged(
337                ActionMode mode, int position, long id, boolean checked) {
338            if (checked) {
339                // Directories cannot be checked
340                final Cursor cursor = mAdapter.getItem(position);
341                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
342                if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
343                    mCurrentView.setItemChecked(position, false);
344                }
345            }
346
347            mode.setTitle(getResources()
348                    .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
349        }
350    };
351
352    private void onShareDocuments(List<DocumentInfo> docs) {
353        Intent intent;
354        if (docs.size() == 1) {
355            final DocumentInfo doc = docs.get(0);
356
357            intent = new Intent(Intent.ACTION_SEND);
358            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
359            intent.addCategory(Intent.CATEGORY_DEFAULT);
360            intent.setType(doc.mimeType);
361            intent.putExtra(Intent.EXTRA_STREAM, doc.uri);
362
363        } else if (docs.size() > 1) {
364            intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
365            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
366            intent.addCategory(Intent.CATEGORY_DEFAULT);
367
368            final ArrayList<String> mimeTypes = Lists.newArrayList();
369            final ArrayList<Uri> uris = Lists.newArrayList();
370            for (DocumentInfo doc : docs) {
371                mimeTypes.add(doc.mimeType);
372                uris.add(doc.uri);
373            }
374
375            intent.setType(findCommonMimeType(mimeTypes));
376            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
377
378        } else {
379            return;
380        }
381
382        intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
383        startActivity(intent);
384    }
385
386    private void onDeleteDocuments(List<DocumentInfo> docs) {
387        final Context context = getActivity();
388        final ContentResolver resolver = context.getContentResolver();
389
390        boolean hadTrouble = false;
391        for (DocumentInfo doc : docs) {
392            if (!doc.isDeleteSupported()) {
393                Log.w(TAG, "Skipping " + doc);
394                hadTrouble = true;
395                continue;
396            }
397
398            try {
399                if (resolver.delete(doc.uri, null, null) != 1) {
400                    Log.w(TAG, "Failed to delete " + doc);
401                    hadTrouble = true;
402                }
403            } catch (Exception e) {
404                Log.w(TAG, "Failed to delete " + doc + ": " + e);
405                hadTrouble = true;
406            }
407        }
408
409        if (hadTrouble) {
410            Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
411        }
412    }
413
414    private static State getDisplayState(Fragment fragment) {
415        return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
416    }
417
418    private class DocumentsAdapter extends BaseAdapter {
419        private Cursor mCursor;
420
421        public void swapCursor(Cursor cursor) {
422            mCursor = cursor;
423
424            if (isEmpty()) {
425                mEmptyView.setVisibility(View.VISIBLE);
426            } else {
427                mEmptyView.setVisibility(View.GONE);
428            }
429
430            notifyDataSetChanged();
431        }
432
433        @Override
434        public View getView(int position, View convertView, ViewGroup parent) {
435            final Context context = parent.getContext();
436            final State state = getDisplayState(DirectoryFragment.this);
437
438            final RootsCache roots = DocumentsApplication.getRootsCache(context);
439            final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
440                    context, mThumbSize);
441
442            if (convertView == null) {
443                final LayoutInflater inflater = LayoutInflater.from(context);
444                if (state.mode == MODE_LIST) {
445                    convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
446                } else if (state.mode == MODE_GRID) {
447                    convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
448                } else {
449                    throw new IllegalStateException();
450                }
451            }
452
453            final Cursor cursor = getItem(position);
454
455            final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
456            final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
457            final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
458            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
459            final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
460            final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
461            final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
462            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
463            final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
464            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
465
466            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
467            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
468            final View summaryGrid = convertView.findViewById(R.id.summary_grid);
469            final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
470            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
471            final TextView date = (TextView) convertView.findViewById(R.id.date);
472            final TextView size = (TextView) convertView.findViewById(R.id.size);
473
474            final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) icon.getTag();
475            if (oldTask != null) {
476                oldTask.cancel(false);
477            }
478
479            if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) {
480                final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
481                final Bitmap cachedResult = thumbs.get(uri);
482                if (cachedResult != null) {
483                    icon.setImageBitmap(cachedResult);
484                } else {
485                    final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize);
486                    icon.setImageBitmap(null);
487                    icon.setTag(task);
488                    task.execute(uri);
489                }
490            } else if (docIcon != 0) {
491                icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon));
492            } else {
493                icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType));
494            }
495
496            title.setText(docDisplayName);
497
498            if (mType == TYPE_RECENT_OPEN) {
499                final RootInfo root = roots.getRoot(docAuthority, docRootId);
500                icon1.setVisibility(View.VISIBLE);
501                icon1.setImageDrawable(root.loadIcon(context));
502                summary.setText(root.getDirectoryString());
503                summary.setVisibility(View.VISIBLE);
504            } else {
505                icon1.setVisibility(View.GONE);
506                if (docSummary != null) {
507                    summary.setText(docSummary);
508                    summary.setVisibility(View.VISIBLE);
509                } else {
510                    summary.setVisibility(View.INVISIBLE);
511                }
512            }
513
514            if (summaryGrid != null) {
515                summaryGrid.setVisibility(
516                        (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE);
517            }
518
519            if (docLastModified == -1) {
520                date.setText(null);
521            } else {
522                date.setText(formatTime(context, docLastModified));
523            }
524
525            if (state.showSize) {
526                size.setVisibility(View.VISIBLE);
527                if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
528                    size.setText(null);
529                } else {
530                    size.setText(Formatter.formatFileSize(context, docSize));
531                }
532            } else {
533                size.setVisibility(View.GONE);
534            }
535
536            return convertView;
537        }
538
539        @Override
540        public int getCount() {
541            return mCursor != null ? mCursor.getCount() : 0;
542        }
543
544        @Override
545        public Cursor getItem(int position) {
546            if (mCursor != null) {
547                mCursor.moveToPosition(position);
548            }
549            return mCursor;
550        }
551
552        @Override
553        public long getItemId(int position) {
554            return position;
555        }
556    }
557
558    private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> {
559        private final ImageView mTarget;
560        private final Point mThumbSize;
561
562        public ThumbnailAsyncTask(ImageView target, Point thumbSize) {
563            mTarget = target;
564            mThumbSize = thumbSize;
565        }
566
567        @Override
568        protected void onPreExecute() {
569            mTarget.setTag(this);
570        }
571
572        @Override
573        protected Bitmap doInBackground(Uri... params) {
574            final Context context = mTarget.getContext();
575            final Uri uri = params[0];
576
577            Bitmap result = null;
578            try {
579                result = DocumentsContract.getDocumentThumbnail(
580                        context.getContentResolver(), uri, mThumbSize, null);
581                if (result != null) {
582                    final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
583                            context, mThumbSize);
584                    thumbs.put(uri, result);
585                }
586            } catch (Exception e) {
587                Log.w(TAG, "Failed to load thumbnail: " + e);
588            }
589            return result;
590        }
591
592        @Override
593        protected void onPostExecute(Bitmap result) {
594            if (mTarget.getTag() == this) {
595                mTarget.setImageBitmap(result);
596                mTarget.setTag(null);
597            }
598        }
599    }
600
601    private static String formatTime(Context context, long when) {
602        // TODO: DateUtils should make this easier
603        Time then = new Time();
604        then.set(when);
605        Time now = new Time();
606        now.setToNow();
607
608        int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
609                | DateUtils.FORMAT_ABBREV_ALL;
610
611        if (then.year != now.year) {
612            flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
613        } else if (then.yearDay != now.yearDay) {
614            flags |= DateUtils.FORMAT_SHOW_DATE;
615        } else {
616            flags |= DateUtils.FORMAT_SHOW_TIME;
617        }
618
619        return DateUtils.formatDateTime(context, when, flags);
620    }
621
622    private String findCommonMimeType(List<String> mimeTypes) {
623        String[] commonType = mimeTypes.get(0).split("/");
624        if (commonType.length != 2) {
625            return "*/*";
626        }
627
628        for (int i = 1; i < mimeTypes.size(); i++) {
629            String[] type = mimeTypes.get(i).split("/");
630            if (type.length != 2) continue;
631
632            if (!commonType[1].equals(type[1])) {
633                commonType[1] = "*";
634            }
635
636            if (!commonType[0].equals(type[0])) {
637                commonType[0] = "*";
638                commonType[1] = "*";
639                break;
640            }
641        }
642
643        return commonType[0] + "/" + commonType[1];
644    }
645}
646