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