DirectoryFragment.java revision d99109fca847895233b0bdfafa131ebca8dfe3d5
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.BaseActivity.State.ACTION_CREATE;
21import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
22import static com.android.documentsui.BaseActivity.State.MODE_GRID;
23import static com.android.documentsui.BaseActivity.State.MODE_LIST;
24import static com.android.documentsui.BaseActivity.State.MODE_UNKNOWN;
25import static com.android.documentsui.BaseActivity.State.SORT_ORDER_UNKNOWN;
26import static com.android.documentsui.model.DocumentInfo.getCursorInt;
27import static com.android.documentsui.model.DocumentInfo.getCursorLong;
28import static com.android.documentsui.model.DocumentInfo.getCursorString;
29
30import android.app.ActivityManager;
31import android.app.Fragment;
32import android.app.FragmentManager;
33import android.app.FragmentTransaction;
34import android.app.LoaderManager.LoaderCallbacks;
35import android.content.ContentProviderClient;
36import android.content.ContentResolver;
37import android.content.ContentValues;
38import android.content.Context;
39import android.content.Intent;
40import android.content.Loader;
41import android.content.res.Resources;
42import android.database.Cursor;
43import android.graphics.Bitmap;
44import android.graphics.Point;
45import android.graphics.drawable.Drawable;
46import android.graphics.drawable.InsetDrawable;
47import android.net.Uri;
48import android.os.AsyncTask;
49import android.os.Bundle;
50import android.os.CancellationSignal;
51import android.os.OperationCanceledException;
52import android.os.Parcelable;
53import android.os.SystemProperties;
54import android.provider.DocumentsContract;
55import android.provider.DocumentsContract.Document;
56import android.text.format.DateUtils;
57import android.text.format.Formatter;
58import android.text.format.Time;
59import android.util.Log;
60import android.util.SparseArray;
61import android.util.SparseBooleanArray;
62import android.view.ActionMode;
63import android.view.LayoutInflater;
64import android.view.Menu;
65import android.view.MenuItem;
66import android.view.View;
67import android.view.ViewGroup;
68import android.widget.AbsListView;
69import android.widget.AbsListView.MultiChoiceModeListener;
70import android.widget.AbsListView.RecyclerListener;
71import android.widget.AdapterView;
72import android.widget.AdapterView.OnItemClickListener;
73import android.widget.BaseAdapter;
74import android.widget.GridView;
75import android.widget.ImageView;
76import android.widget.ListView;
77import android.widget.TextView;
78import android.widget.Toast;
79
80import com.android.documentsui.BaseActivity.State;
81import com.android.documentsui.CopyService;
82import com.android.documentsui.ProviderExecutor.Preemptable;
83import com.android.documentsui.RecentsProvider.StateColumns;
84import com.android.documentsui.model.DocumentInfo;
85import com.android.documentsui.model.RootInfo;
86import com.google.android.collect.Lists;
87
88import java.util.ArrayList;
89import java.util.List;
90
91/**
92 * Display the documents inside a single directory.
93 */
94public class DirectoryFragment extends Fragment {
95
96    private View mEmptyView;
97    private ListView mListView;
98    private GridView mGridView;
99
100    private AbsListView mCurrentView;
101
102    public static final int TYPE_NORMAL = 1;
103    public static final int TYPE_SEARCH = 2;
104    public static final int TYPE_RECENT_OPEN = 3;
105
106    public static final int ANIM_NONE = 1;
107    public static final int ANIM_SIDE = 2;
108    public static final int ANIM_DOWN = 3;
109    public static final int ANIM_UP = 4;
110
111    private int mType = TYPE_NORMAL;
112    private String mStateKey;
113
114    private int mLastMode = MODE_UNKNOWN;
115    private int mLastSortOrder = SORT_ORDER_UNKNOWN;
116    private boolean mLastShowSize = false;
117
118    private boolean mHideGridTitles = false;
119
120    private boolean mSvelteRecents;
121    private Point mThumbSize;
122
123    private DocumentsAdapter mAdapter;
124    private LoaderCallbacks<DirectoryResult> mCallbacks;
125
126    private static final String EXTRA_TYPE = "type";
127    private static final String EXTRA_ROOT = "root";
128    private static final String EXTRA_DOC = "doc";
129    private static final String EXTRA_QUERY = "query";
130    private static final String EXTRA_IGNORE_STATE = "ignoreState";
131
132    private final int mLoaderId = 42;
133
134    public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
135        show(fm, TYPE_NORMAL, root, doc, null, anim);
136    }
137
138    public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
139        show(fm, TYPE_SEARCH, root, null, query, anim);
140    }
141
142    public static void showRecentsOpen(FragmentManager fm, int anim) {
143        show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
144    }
145
146    private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
147            String query, int anim) {
148        final Bundle args = new Bundle();
149        args.putInt(EXTRA_TYPE, type);
150        args.putParcelable(EXTRA_ROOT, root);
151        args.putParcelable(EXTRA_DOC, doc);
152        args.putString(EXTRA_QUERY, query);
153
154        final FragmentTransaction ft = fm.beginTransaction();
155        switch (anim) {
156            case ANIM_SIDE:
157                args.putBoolean(EXTRA_IGNORE_STATE, true);
158                break;
159            case ANIM_DOWN:
160                args.putBoolean(EXTRA_IGNORE_STATE, true);
161                ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen);
162                break;
163            case ANIM_UP:
164                ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up);
165                break;
166        }
167
168        final DirectoryFragment fragment = new DirectoryFragment();
169        fragment.setArguments(args);
170
171        ft.replace(R.id.container_directory, fragment);
172        ft.commitAllowingStateLoss();
173    }
174
175    private static String buildStateKey(RootInfo root, DocumentInfo doc) {
176        final StringBuilder builder = new StringBuilder();
177        builder.append(root != null ? root.authority : "null").append(';');
178        builder.append(root != null ? root.rootId : "null").append(';');
179        builder.append(doc != null ? doc.documentId : "null");
180        return builder.toString();
181    }
182
183    public static DirectoryFragment get(FragmentManager fm) {
184        // TODO: deal with multiple directories shown at once
185        return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
186    }
187
188    @Override
189    public View onCreateView(
190            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
191        final Context context = inflater.getContext();
192        final Resources res = context.getResources();
193        final View view = inflater.inflate(R.layout.fragment_directory, container, false);
194
195        mEmptyView = view.findViewById(android.R.id.empty);
196
197        mListView = (ListView) view.findViewById(R.id.list);
198        mListView.setOnItemClickListener(mItemListener);
199        mListView.setMultiChoiceModeListener(mMultiListener);
200        mListView.setRecyclerListener(mRecycleListener);
201
202        // Indent our list divider to align with text
203        final Drawable divider = mListView.getDivider();
204        final boolean insetLeft = res.getBoolean(R.bool.list_divider_inset_left);
205        final int insetSize = res.getDimensionPixelSize(R.dimen.list_divider_inset);
206        if (insetLeft) {
207            mListView.setDivider(new InsetDrawable(divider, insetSize, 0, 0, 0));
208        } else {
209            mListView.setDivider(new InsetDrawable(divider, 0, 0, insetSize, 0));
210        }
211
212        mGridView = (GridView) view.findViewById(R.id.grid);
213        mGridView.setOnItemClickListener(mItemListener);
214        mGridView.setMultiChoiceModeListener(mMultiListener);
215        mGridView.setRecyclerListener(mRecycleListener);
216
217        return view;
218    }
219
220    @Override
221    public void onDestroyView() {
222        super.onDestroyView();
223
224        // Cancel any outstanding thumbnail requests
225        final ViewGroup target = (mListView.getAdapter() != null) ? mListView : mGridView;
226        final int count = target.getChildCount();
227        for (int i = 0; i < count; i++) {
228            final View view = target.getChildAt(i);
229            mRecycleListener.onMovedToScrapHeap(view);
230        }
231
232        // Tear down any selection in progress
233        mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
234        mGridView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
235    }
236
237    @Override
238    public void onActivityCreated(Bundle savedInstanceState) {
239        super.onActivityCreated(savedInstanceState);
240
241        final Context context = getActivity();
242        final State state = getDisplayState(DirectoryFragment.this);
243
244        final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
245        final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
246
247        mAdapter = new DocumentsAdapter();
248        mType = getArguments().getInt(EXTRA_TYPE);
249        mStateKey = buildStateKey(root, doc);
250
251        if (mType == TYPE_RECENT_OPEN) {
252            // Hide titles when showing recents for picking images/videos
253            mHideGridTitles = MimePredicate.mimeMatches(
254                    MimePredicate.VISUAL_MIMES, state.acceptMimes);
255        } else {
256            mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
257        }
258
259        final ActivityManager am = (ActivityManager) context.getSystemService(
260                Context.ACTIVITY_SERVICE);
261        mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
262
263        mCallbacks = new LoaderCallbacks<DirectoryResult>() {
264            @Override
265            public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
266                final String query = getArguments().getString(EXTRA_QUERY);
267
268                Uri contentsUri;
269                switch (mType) {
270                    case TYPE_NORMAL:
271                        contentsUri = DocumentsContract.buildChildDocumentsUri(
272                                doc.authority, doc.documentId);
273                        if (state.action == ACTION_MANAGE) {
274                            contentsUri = DocumentsContract.setManageMode(contentsUri);
275                        }
276                        return new DirectoryLoader(
277                                context, mType, root, doc, contentsUri, state.userSortOrder);
278                    case TYPE_SEARCH:
279                        contentsUri = DocumentsContract.buildSearchDocumentsUri(
280                                root.authority, root.rootId, query);
281                        if (state.action == ACTION_MANAGE) {
282                            contentsUri = DocumentsContract.setManageMode(contentsUri);
283                        }
284                        return new DirectoryLoader(
285                                context, mType, root, doc, contentsUri, state.userSortOrder);
286                    case TYPE_RECENT_OPEN:
287                        final RootsCache roots = DocumentsApplication.getRootsCache(context);
288                        return new RecentLoader(context, roots, state);
289                    default:
290                        throw new IllegalStateException("Unknown type " + mType);
291                }
292            }
293
294            @Override
295            public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
296                if (!isAdded()) return;
297
298                mAdapter.swapResult(result);
299
300                // Push latest state up to UI
301                // TODO: if mode change was racing with us, don't overwrite it
302                if (result.mode != MODE_UNKNOWN) {
303                    state.derivedMode = result.mode;
304                }
305                state.derivedSortOrder = result.sortOrder;
306                ((BaseActivity) context).onStateChanged();
307
308                updateDisplayState();
309
310                // When launched into empty recents, show drawer
311                if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) {
312                    ((BaseActivity) context).setRootsDrawerOpen(true);
313                }
314
315                // Restore any previous instance state
316                final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
317                if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
318                    getView().restoreHierarchyState(container);
319                } else if (mLastSortOrder != state.derivedSortOrder) {
320                    mListView.smoothScrollToPosition(0);
321                    mGridView.smoothScrollToPosition(0);
322                }
323
324                mLastSortOrder = state.derivedSortOrder;
325            }
326
327            @Override
328            public void onLoaderReset(Loader<DirectoryResult> loader) {
329                mAdapter.swapResult(null);
330            }
331        };
332
333        // Kick off loader at least once
334        getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
335
336        updateDisplayState();
337    }
338
339    @Override
340    public void onStop() {
341        super.onStop();
342
343        // Remember last scroll location
344        final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
345        getView().saveHierarchyState(container);
346        final State state = getDisplayState(this);
347        state.dirState.put(mStateKey, container);
348    }
349
350    @Override
351    public void onResume() {
352        super.onResume();
353        updateDisplayState();
354    }
355
356    public void onDisplayStateChanged() {
357        updateDisplayState();
358    }
359
360    public void onUserSortOrderChanged() {
361        // Sort order change always triggers reload; we'll trigger state change
362        // on the flip side.
363        getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
364    }
365
366    public void onUserModeChanged() {
367        final ContentResolver resolver = getActivity().getContentResolver();
368        final State state = getDisplayState(this);
369
370        final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
371        final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
372
373        if (root != null && doc != null) {
374            final Uri stateUri = RecentsProvider.buildState(
375                    root.authority, root.rootId, doc.documentId);
376            final ContentValues values = new ContentValues();
377            values.put(StateColumns.MODE, state.userMode);
378
379            new AsyncTask<Void, Void, Void>() {
380                @Override
381                protected Void doInBackground(Void... params) {
382                    resolver.insert(stateUri, values);
383                    return null;
384                }
385            }.execute();
386        }
387
388        // Mode change is just visual change; no need to kick loader, and
389        // deliver change event immediately.
390        state.derivedMode = state.userMode;
391        ((BaseActivity) getActivity()).onStateChanged();
392
393        updateDisplayState();
394    }
395
396    private void updateDisplayState() {
397        final State state = getDisplayState(this);
398
399        if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return;
400        mLastMode = state.derivedMode;
401        mLastShowSize = state.showSize;
402
403        mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE);
404        mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE);
405
406        final int choiceMode;
407        if (state.allowMultiple) {
408            choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
409        } else {
410            choiceMode = ListView.CHOICE_MODE_NONE;
411        }
412
413        final int thumbSize;
414        if (state.derivedMode == MODE_GRID) {
415            thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
416            mListView.setAdapter(null);
417            mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
418            mGridView.setAdapter(mAdapter);
419            mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
420            mGridView.setNumColumns(GridView.AUTO_FIT);
421            mGridView.setChoiceMode(choiceMode);
422            mCurrentView = mGridView;
423        } else if (state.derivedMode == MODE_LIST) {
424            thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
425            mGridView.setAdapter(null);
426            mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
427            mListView.setAdapter(mAdapter);
428            mListView.setChoiceMode(choiceMode);
429            mCurrentView = mListView;
430        } else {
431            throw new IllegalStateException("Unknown state " + state.derivedMode);
432        }
433
434        mThumbSize = new Point(thumbSize, thumbSize);
435    }
436
437    private OnItemClickListener mItemListener = new OnItemClickListener() {
438        @Override
439        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
440            final Cursor cursor = mAdapter.getItem(position);
441            if (cursor != null) {
442                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
443                final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
444                if (isDocumentEnabled(docMimeType, docFlags)) {
445                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
446                    ((BaseActivity) getActivity()).onDocumentPicked(doc);
447                }
448            }
449        }
450    };
451
452    private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
453        @Override
454        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
455            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
456            mode.setTitle(getResources()
457                    .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
458            return true;
459        }
460
461        @Override
462        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
463            final State state = getDisplayState(DirectoryFragment.this);
464
465            final MenuItem open = menu.findItem(R.id.menu_open);
466            final MenuItem share = menu.findItem(R.id.menu_share);
467            final MenuItem delete = menu.findItem(R.id.menu_delete);
468            final MenuItem copy = menu.findItem(R.id.menu_copy);
469
470            final boolean manageMode = state.action == ACTION_MANAGE;
471            open.setVisible(!manageMode);
472            share.setVisible(manageMode);
473            delete.setVisible(manageMode);
474            // Hide the copy feature by default.
475            copy.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_copy", false));
476
477            return true;
478        }
479
480        @Override
481        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
482            final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
483            final ArrayList<DocumentInfo> docs = Lists.newArrayList();
484            final int size = checked.size();
485            for (int i = 0; i < size; i++) {
486                if (checked.valueAt(i)) {
487                    final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
488                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
489                    docs.add(doc);
490                }
491            }
492
493            final int id = item.getItemId();
494            if (id == R.id.menu_open) {
495                BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
496                mode.finish();
497                return true;
498
499            } else if (id == R.id.menu_share) {
500                onShareDocuments(docs);
501                mode.finish();
502                return true;
503
504            } else if (id == R.id.menu_delete) {
505                onDeleteDocuments(docs);
506                mode.finish();
507                return true;
508
509            } else if (id == R.id.menu_copy) {
510                onCopyDocuments(docs);
511                mode.finish();
512                return true;
513
514            } else if (id == R.id.menu_select_all) {
515                int count = mCurrentView.getCount();
516                for (int i = 0; i < count; i++) {
517                    mCurrentView.setItemChecked(i, true);
518                }
519                updateDisplayState();
520                return true;
521
522            } else {
523                return false;
524            }
525        }
526
527        @Override
528        public void onDestroyActionMode(ActionMode mode) {
529            // ignored
530        }
531
532        @Override
533        public void onItemCheckedStateChanged(
534                ActionMode mode, int position, long id, boolean checked) {
535            if (checked) {
536                // Directories and footer items cannot be checked
537                boolean valid = false;
538
539                final Cursor cursor = mAdapter.getItem(position);
540                if (cursor != null) {
541                    final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
542                    final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
543                    if (!Document.MIME_TYPE_DIR.equals(docMimeType)) {
544                        valid = isDocumentEnabled(docMimeType, docFlags);
545                    }
546                }
547
548                if (!valid) {
549                    mCurrentView.setItemChecked(position, false);
550                }
551            }
552
553            mode.setTitle(getResources()
554                    .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
555        }
556    };
557
558    private RecyclerListener mRecycleListener = new RecyclerListener() {
559        @Override
560        public void onMovedToScrapHeap(View view) {
561            final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
562            if (iconThumb != null) {
563                final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
564                if (oldTask != null) {
565                    oldTask.preempt();
566                    iconThumb.setTag(null);
567                }
568            }
569        }
570    };
571
572    private void onShareDocuments(List<DocumentInfo> docs) {
573        Intent intent;
574        if (docs.size() == 1) {
575            final DocumentInfo doc = docs.get(0);
576
577            intent = new Intent(Intent.ACTION_SEND);
578            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
579            intent.addCategory(Intent.CATEGORY_DEFAULT);
580            intent.setType(doc.mimeType);
581            intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
582
583        } else if (docs.size() > 1) {
584            intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
585            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
586            intent.addCategory(Intent.CATEGORY_DEFAULT);
587
588            final ArrayList<String> mimeTypes = Lists.newArrayList();
589            final ArrayList<Uri> uris = Lists.newArrayList();
590            for (DocumentInfo doc : docs) {
591                mimeTypes.add(doc.mimeType);
592                uris.add(doc.derivedUri);
593            }
594
595            intent.setType(findCommonMimeType(mimeTypes));
596            intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
597
598        } else {
599            return;
600        }
601
602        intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
603        startActivity(intent);
604    }
605
606    private void onDeleteDocuments(List<DocumentInfo> docs) {
607        final Context context = getActivity();
608        final ContentResolver resolver = context.getContentResolver();
609
610        boolean hadTrouble = false;
611        for (DocumentInfo doc : docs) {
612            if (!doc.isDeleteSupported()) {
613                Log.w(TAG, "Skipping " + doc);
614                hadTrouble = true;
615                continue;
616            }
617
618            ContentProviderClient client = null;
619            try {
620                client = DocumentsApplication.acquireUnstableProviderOrThrow(
621                        resolver, doc.derivedUri.getAuthority());
622                DocumentsContract.deleteDocument(client, doc.derivedUri);
623            } catch (Exception e) {
624                Log.w(TAG, "Failed to delete " + doc);
625                hadTrouble = true;
626            } finally {
627                ContentProviderClient.releaseQuietly(client);
628            }
629        }
630
631        if (hadTrouble) {
632            Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
633        }
634    }
635
636    private void onCopyDocuments(List<DocumentInfo> docs) {
637        final Context context = getActivity();
638        final Resources res = context.getResources();
639
640        Intent copyIntent = new Intent(context, CopyService.class);
641        copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST,
642                new ArrayList<DocumentInfo>(docs));
643
644        Toast.makeText(context,
645                res.getQuantityString(R.plurals.copy_begin, docs.size(), docs.size()),
646                Toast.LENGTH_SHORT).show();
647        context.startService(copyIntent);
648    }
649
650    private static State getDisplayState(Fragment fragment) {
651        return ((BaseActivity) fragment.getActivity()).getDisplayState();
652    }
653
654    private static abstract class Footer {
655        private final int mItemViewType;
656
657        public Footer(int itemViewType) {
658            mItemViewType = itemViewType;
659        }
660
661        public abstract View getView(View convertView, ViewGroup parent);
662
663        public int getItemViewType() {
664            return mItemViewType;
665        }
666    }
667
668    private class LoadingFooter extends Footer {
669        public LoadingFooter() {
670            super(1);
671        }
672
673        @Override
674        public View getView(View convertView, ViewGroup parent) {
675            final Context context = parent.getContext();
676            final State state = getDisplayState(DirectoryFragment.this);
677
678            if (convertView == null) {
679                final LayoutInflater inflater = LayoutInflater.from(context);
680                if (state.derivedMode == MODE_LIST) {
681                    convertView = inflater.inflate(R.layout.item_loading_list, parent, false);
682                } else if (state.derivedMode == MODE_GRID) {
683                    convertView = inflater.inflate(R.layout.item_loading_grid, parent, false);
684                } else {
685                    throw new IllegalStateException();
686                }
687            }
688
689            return convertView;
690        }
691    }
692
693    private class MessageFooter extends Footer {
694        private final int mIcon;
695        private final String mMessage;
696
697        public MessageFooter(int itemViewType, int icon, String message) {
698            super(itemViewType);
699            mIcon = icon;
700            mMessage = message;
701        }
702
703        @Override
704        public View getView(View convertView, ViewGroup parent) {
705            final Context context = parent.getContext();
706            final State state = getDisplayState(DirectoryFragment.this);
707
708            if (convertView == null) {
709                final LayoutInflater inflater = LayoutInflater.from(context);
710                if (state.derivedMode == MODE_LIST) {
711                    convertView = inflater.inflate(R.layout.item_message_list, parent, false);
712                } else if (state.derivedMode == MODE_GRID) {
713                    convertView = inflater.inflate(R.layout.item_message_grid, parent, false);
714                } else {
715                    throw new IllegalStateException();
716                }
717            }
718
719            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
720            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
721            icon.setImageResource(mIcon);
722            title.setText(mMessage);
723            return convertView;
724        }
725    }
726
727    private class DocumentsAdapter extends BaseAdapter {
728        private Cursor mCursor;
729        private int mCursorCount;
730
731        private List<Footer> mFooters = Lists.newArrayList();
732
733        public void swapResult(DirectoryResult result) {
734            mCursor = result != null ? result.cursor : null;
735            mCursorCount = mCursor != null ? mCursor.getCount() : 0;
736
737            mFooters.clear();
738
739            final Bundle extras = mCursor != null ? mCursor.getExtras() : null;
740            if (extras != null) {
741                final String info = extras.getString(DocumentsContract.EXTRA_INFO);
742                if (info != null) {
743                    mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info));
744                }
745                final String error = extras.getString(DocumentsContract.EXTRA_ERROR);
746                if (error != null) {
747                    mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error));
748                }
749                if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) {
750                    mFooters.add(new LoadingFooter());
751                }
752            }
753
754            if (result != null && result.exception != null) {
755                mFooters.add(new MessageFooter(
756                        3, R.drawable.ic_dialog_alert, getString(R.string.query_error)));
757            }
758
759            if (isEmpty()) {
760                mEmptyView.setVisibility(View.VISIBLE);
761            } else {
762                mEmptyView.setVisibility(View.GONE);
763            }
764
765            notifyDataSetChanged();
766        }
767
768        @Override
769        public View getView(int position, View convertView, ViewGroup parent) {
770            if (position < mCursorCount) {
771                return getDocumentView(position, convertView, parent);
772            } else {
773                position -= mCursorCount;
774                convertView = mFooters.get(position).getView(convertView, parent);
775                // Only the view itself is disabled; contents inside shouldn't
776                // be dimmed.
777                convertView.setEnabled(false);
778                return convertView;
779            }
780        }
781
782        private View getDocumentView(int position, View convertView, ViewGroup parent) {
783            final Context context = parent.getContext();
784            final State state = getDisplayState(DirectoryFragment.this);
785
786            final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
787
788            final RootsCache roots = DocumentsApplication.getRootsCache(context);
789            final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
790                    context, mThumbSize);
791
792            if (convertView == null) {
793                final LayoutInflater inflater = LayoutInflater.from(context);
794                if (state.derivedMode == MODE_LIST) {
795                    convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
796                } else if (state.derivedMode == MODE_GRID) {
797                    convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
798                } else {
799                    throw new IllegalStateException();
800                }
801            }
802
803            final Cursor cursor = getItem(position);
804
805            final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
806            final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
807            final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
808            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
809            final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
810            final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
811            final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
812            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
813            final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
814            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
815
816            final View line1 = convertView.findViewById(R.id.line1);
817            final View line2 = convertView.findViewById(R.id.line2);
818
819            final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime);
820            final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb);
821            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
822            final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
823            final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2);
824            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
825            final TextView date = (TextView) convertView.findViewById(R.id.date);
826            final TextView size = (TextView) convertView.findViewById(R.id.size);
827
828            final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
829            if (oldTask != null) {
830                oldTask.preempt();
831                iconThumb.setTag(null);
832            }
833
834            iconMime.animate().cancel();
835            iconThumb.animate().cancel();
836
837            final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
838            final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
839                    || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
840            final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
841
842            final boolean enabled = isDocumentEnabled(docMimeType, docFlags);
843            final float iconAlpha = (state.derivedMode == MODE_LIST && !enabled) ? 0.5f : 1f;
844
845            boolean cacheHit = false;
846            if (showThumbnail) {
847                final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
848                final Bitmap cachedResult = thumbs.get(uri);
849                if (cachedResult != null) {
850                    iconThumb.setImageBitmap(cachedResult);
851                    cacheHit = true;
852                } else {
853                    iconThumb.setImageDrawable(null);
854                    final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
855                            uri, iconMime, iconThumb, mThumbSize, iconAlpha);
856                    iconThumb.setTag(task);
857                    ProviderExecutor.forAuthority(docAuthority).execute(task);
858                }
859            }
860
861            // Always throw MIME icon into place, even when a thumbnail is being
862            // loaded in background.
863            if (cacheHit) {
864                iconMime.setAlpha(0f);
865                iconMime.setImageDrawable(null);
866                iconThumb.setAlpha(1f);
867            } else {
868                iconMime.setAlpha(1f);
869                iconThumb.setAlpha(0f);
870                iconThumb.setImageDrawable(null);
871                if (docIcon != 0) {
872                    iconMime.setImageDrawable(
873                            IconUtils.loadPackageIcon(context, docAuthority, docIcon));
874                } else {
875                    iconMime.setImageDrawable(IconUtils.loadMimeIcon(
876                            context, docMimeType, docAuthority, docId, state.derivedMode));
877                }
878            }
879
880            boolean hasLine1 = false;
881            boolean hasLine2 = false;
882
883            final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles;
884            if (!hideTitle) {
885                title.setText(docDisplayName);
886                hasLine1 = true;
887            }
888
889            Drawable iconDrawable = null;
890            if (mType == TYPE_RECENT_OPEN) {
891                // We've already had to enumerate roots before any results can
892                // be shown, so this will never block.
893                final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
894                if (state.derivedMode == MODE_GRID) {
895                    iconDrawable = root.loadGridIcon(context);
896                } else {
897                    iconDrawable = root.loadIcon(context);
898                }
899
900                if (summary != null) {
901                    final boolean alwaysShowSummary = getResources()
902                            .getBoolean(R.bool.always_show_summary);
903                    if (alwaysShowSummary) {
904                        summary.setText(root.getDirectoryString());
905                        summary.setVisibility(View.VISIBLE);
906                        hasLine2 = true;
907                    } else {
908                        if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
909                            // No summary needed if icon speaks for itself
910                            summary.setVisibility(View.INVISIBLE);
911                        } else {
912                            summary.setText(root.getDirectoryString());
913                            summary.setVisibility(View.VISIBLE);
914                            summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
915                            hasLine2 = true;
916                        }
917                    }
918                }
919            } else {
920                // Directories showing thumbnails in grid mode get a little icon
921                // hint to remind user they're a directory.
922                if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
923                        && showThumbnail) {
924                    iconDrawable = IconUtils.applyTintAttr(context, R.drawable.ic_doc_folder,
925                            android.R.attr.textColorPrimaryInverse);
926                }
927
928                if (summary != null) {
929                    if (docSummary != null) {
930                        summary.setText(docSummary);
931                        summary.setVisibility(View.VISIBLE);
932                        hasLine2 = true;
933                    } else {
934                        summary.setVisibility(View.INVISIBLE);
935                    }
936                }
937            }
938
939            if (icon1 != null) icon1.setVisibility(View.GONE);
940            if (icon2 != null) icon2.setVisibility(View.GONE);
941
942            if (iconDrawable != null) {
943                if (hasLine1) {
944                    icon1.setVisibility(View.VISIBLE);
945                    icon1.setImageDrawable(iconDrawable);
946                } else {
947                    icon2.setVisibility(View.VISIBLE);
948                    icon2.setImageDrawable(iconDrawable);
949                }
950            }
951
952            if (docLastModified == -1) {
953                date.setText(null);
954            } else {
955                date.setText(formatTime(context, docLastModified));
956                hasLine2 = true;
957            }
958
959            if (state.showSize) {
960                size.setVisibility(View.VISIBLE);
961                if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
962                    size.setText(null);
963                } else {
964                    size.setText(Formatter.formatFileSize(context, docSize));
965                    hasLine2 = true;
966                }
967            } else {
968                size.setVisibility(View.GONE);
969            }
970
971            if (line1 != null) {
972                line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE);
973            }
974            if (line2 != null) {
975                line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE);
976            }
977
978            setEnabledRecursive(convertView, enabled);
979
980            iconMime.setAlpha(iconAlpha);
981            iconThumb.setAlpha(iconAlpha);
982            if (icon1 != null) icon1.setAlpha(iconAlpha);
983            if (icon2 != null) icon2.setAlpha(iconAlpha);
984
985            return convertView;
986        }
987
988        @Override
989        public int getCount() {
990            return mCursorCount + mFooters.size();
991        }
992
993        @Override
994        public Cursor getItem(int position) {
995            if (position < mCursorCount) {
996                mCursor.moveToPosition(position);
997                return mCursor;
998            } else {
999                return null;
1000            }
1001        }
1002
1003        @Override
1004        public long getItemId(int position) {
1005            return position;
1006        }
1007
1008        @Override
1009        public int getViewTypeCount() {
1010            return 4;
1011        }
1012
1013        @Override
1014        public int getItemViewType(int position) {
1015            if (position < mCursorCount) {
1016                return 0;
1017            } else {
1018                position -= mCursorCount;
1019                return mFooters.get(position).getItemViewType();
1020            }
1021        }
1022    }
1023
1024    private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
1025            implements Preemptable {
1026        private final Uri mUri;
1027        private final ImageView mIconMime;
1028        private final ImageView mIconThumb;
1029        private final Point mThumbSize;
1030        private final float mTargetAlpha;
1031        private final CancellationSignal mSignal;
1032
1033        public ThumbnailAsyncTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize,
1034                float targetAlpha) {
1035            mUri = uri;
1036            mIconMime = iconMime;
1037            mIconThumb = iconThumb;
1038            mThumbSize = thumbSize;
1039            mTargetAlpha = targetAlpha;
1040            mSignal = new CancellationSignal();
1041        }
1042
1043        @Override
1044        public void preempt() {
1045            cancel(false);
1046            mSignal.cancel();
1047        }
1048
1049        @Override
1050        protected Bitmap doInBackground(Uri... params) {
1051            if (isCancelled()) return null;
1052
1053            final Context context = mIconThumb.getContext();
1054            final ContentResolver resolver = context.getContentResolver();
1055
1056            ContentProviderClient client = null;
1057            Bitmap result = null;
1058            try {
1059                client = DocumentsApplication.acquireUnstableProviderOrThrow(
1060                        resolver, mUri.getAuthority());
1061                result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
1062                if (result != null) {
1063                    final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
1064                            context, mThumbSize);
1065                    thumbs.put(mUri, result);
1066                }
1067            } catch (Exception e) {
1068                if (!(e instanceof OperationCanceledException)) {
1069                    Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
1070                }
1071            } finally {
1072                ContentProviderClient.releaseQuietly(client);
1073            }
1074            return result;
1075        }
1076
1077        @Override
1078        protected void onPostExecute(Bitmap result) {
1079            if (mIconThumb.getTag() == this && result != null) {
1080                mIconThumb.setTag(null);
1081                mIconThumb.setImageBitmap(result);
1082
1083                mIconMime.setAlpha(mTargetAlpha);
1084                mIconMime.animate().alpha(0f).start();
1085                mIconThumb.setAlpha(0f);
1086                mIconThumb.animate().alpha(mTargetAlpha).start();
1087            }
1088        }
1089    }
1090
1091    private static String formatTime(Context context, long when) {
1092        // TODO: DateUtils should make this easier
1093        Time then = new Time();
1094        then.set(when);
1095        Time now = new Time();
1096        now.setToNow();
1097
1098        int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
1099                | DateUtils.FORMAT_ABBREV_ALL;
1100
1101        if (then.year != now.year) {
1102            flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
1103        } else if (then.yearDay != now.yearDay) {
1104            flags |= DateUtils.FORMAT_SHOW_DATE;
1105        } else {
1106            flags |= DateUtils.FORMAT_SHOW_TIME;
1107        }
1108
1109        return DateUtils.formatDateTime(context, when, flags);
1110    }
1111
1112    private String findCommonMimeType(List<String> mimeTypes) {
1113        String[] commonType = mimeTypes.get(0).split("/");
1114        if (commonType.length != 2) {
1115            return "*/*";
1116        }
1117
1118        for (int i = 1; i < mimeTypes.size(); i++) {
1119            String[] type = mimeTypes.get(i).split("/");
1120            if (type.length != 2) continue;
1121
1122            if (!commonType[1].equals(type[1])) {
1123                commonType[1] = "*";
1124            }
1125
1126            if (!commonType[0].equals(type[0])) {
1127                commonType[0] = "*";
1128                commonType[1] = "*";
1129                break;
1130            }
1131        }
1132
1133        return commonType[0] + "/" + commonType[1];
1134    }
1135
1136    private void setEnabledRecursive(View v, boolean enabled) {
1137        if (v == null) return;
1138        if (v.isEnabled() == enabled) return;
1139        v.setEnabled(enabled);
1140
1141        if (v instanceof ViewGroup) {
1142            final ViewGroup vg = (ViewGroup) v;
1143            for (int i = vg.getChildCount() - 1; i >= 0; i--) {
1144                setEnabledRecursive(vg.getChildAt(i), enabled);
1145            }
1146        }
1147    }
1148
1149    private boolean isDocumentEnabled(String docMimeType, int docFlags) {
1150        final State state = getDisplayState(DirectoryFragment.this);
1151
1152        // Directories are always enabled
1153        if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1154            return true;
1155        }
1156
1157        // Read-only files are disabled when creating
1158        if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
1159            return false;
1160        }
1161
1162        return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
1163    }
1164}
1165