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