[go: nahoru, domu]

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.dirlist;
18
19import static com.android.documentsui.Shared.DEBUG;
20import static com.android.documentsui.Shared.MAX_DOCS_IN_INTENT;
21import static com.android.documentsui.State.MODE_GRID;
22import static com.android.documentsui.State.MODE_LIST;
23import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
24import static com.android.documentsui.model.DocumentInfo.getCursorInt;
25import static com.android.documentsui.model.DocumentInfo.getCursorString;
26
27import android.annotation.IntDef;
28import android.annotation.StringRes;
29import android.app.Activity;
30import android.app.ActivityManager;
31import android.app.AlertDialog;
32import android.app.Fragment;
33import android.app.FragmentManager;
34import android.app.FragmentTransaction;
35import android.app.LoaderManager.LoaderCallbacks;
36import android.content.ClipData;
37import android.content.Context;
38import android.content.DialogInterface;
39import android.content.Intent;
40import android.content.Loader;
41import android.database.Cursor;
42import android.graphics.Canvas;
43import android.graphics.Point;
44import android.graphics.Rect;
45import android.graphics.drawable.Drawable;
46import android.net.Uri;
47import android.os.AsyncTask;
48import android.os.Bundle;
49import android.os.Parcel;
50import android.os.Parcelable;
51import android.provider.DocumentsContract;
52import android.provider.DocumentsContract.Document;
53import android.support.annotation.Nullable;
54import android.support.design.widget.Snackbar;
55import android.support.v13.view.DragStartHelper;
56import android.support.v7.widget.GridLayoutManager;
57import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
58import android.support.v7.widget.RecyclerView;
59import android.support.v7.widget.RecyclerView.OnItemTouchListener;
60import android.support.v7.widget.RecyclerView.RecyclerListener;
61import android.support.v7.widget.RecyclerView.ViewHolder;
62import android.text.BidiFormatter;
63import android.text.TextUtils;
64import android.util.Log;
65import android.util.SparseArray;
66import android.view.ActionMode;
67import android.view.DragEvent;
68import android.view.GestureDetector;
69import android.view.HapticFeedbackConstants;
70import android.view.KeyEvent;
71import android.view.LayoutInflater;
72import android.view.Menu;
73import android.view.MenuItem;
74import android.view.MotionEvent;
75import android.view.View;
76import android.view.ViewGroup;
77import android.widget.ImageView;
78import android.widget.TextView;
79import android.widget.Toolbar;
80
81import com.android.documentsui.BaseActivity;
82import com.android.documentsui.DirectoryLoader;
83import com.android.documentsui.DirectoryResult;
84import com.android.documentsui.DocumentClipper;
85import com.android.documentsui.DocumentsActivity;
86import com.android.documentsui.DocumentsApplication;
87import com.android.documentsui.Events;
88import com.android.documentsui.Events.MotionInputEvent;
89import com.android.documentsui.Menus;
90import com.android.documentsui.MessageBar;
91import com.android.documentsui.Metrics;
92import com.android.documentsui.MimePredicate;
93import com.android.documentsui.R;
94import com.android.documentsui.RecentsLoader;
95import com.android.documentsui.RootsCache;
96import com.android.documentsui.Shared;
97import com.android.documentsui.Snackbars;
98import com.android.documentsui.State;
99import com.android.documentsui.State.ViewMode;
100import com.android.documentsui.dirlist.MultiSelectManager.Selection;
101import com.android.documentsui.model.DocumentInfo;
102import com.android.documentsui.model.DocumentStack;
103import com.android.documentsui.model.RootInfo;
104import com.android.documentsui.services.FileOperationService;
105import com.android.documentsui.services.FileOperationService.OpType;
106import com.android.documentsui.services.FileOperations;
107
108import com.google.common.collect.Lists;
109
110import java.lang.annotation.Retention;
111import java.lang.annotation.RetentionPolicy;
112import java.util.ArrayList;
113import java.util.Collections;
114import java.util.HashSet;
115import java.util.List;
116import java.util.Objects;
117import java.util.Set;
118
119/**
120 * Display the documents inside a single directory.
121 */
122public class DirectoryFragment extends Fragment
123        implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
124
125    @IntDef(flag = true, value = {
126            TYPE_NORMAL,
127            TYPE_RECENT_OPEN
128    })
129    @Retention(RetentionPolicy.SOURCE)
130    public @interface ResultType {}
131    public static final int TYPE_NORMAL = 1;
132    public static final int TYPE_RECENT_OPEN = 2;
133
134    @IntDef(flag = true, value = {
135            REQUEST_COPY_DESTINATION
136    })
137    @Retention(RetentionPolicy.SOURCE)
138    public @interface RequestCode {}
139    public static final int REQUEST_COPY_DESTINATION = 1;
140
141    private static final String TAG = "DirectoryFragment";
142    private static final int LOADER_ID = 42;
143
144    private Model mModel;
145    private MultiSelectManager mSelectionManager;
146    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
147    private ItemEventListener mItemEventListener = new ItemEventListener();
148    private FocusManager mFocusManager;
149
150    private IconHelper mIconHelper;
151
152    private View mEmptyView;
153    private RecyclerView mRecView;
154    private ListeningGestureDetector mGestureDetector;
155
156    private String mStateKey;
157
158    private int mLastSortOrder = SORT_ORDER_UNKNOWN;
159    private DocumentsAdapter mAdapter;
160    private FragmentTuner mTuner;
161    private DocumentClipper mClipper;
162    private GridLayoutManager mLayout;
163    private int mColumnCount = 1;  // This will get updated when layout changes.
164
165    private LayoutInflater mInflater;
166    private MessageBar mMessageBar;
167    private View mProgressBar;
168
169    // Directory fragment state is defined by: root, document, query, type, selection
170    private @ResultType int mType = TYPE_NORMAL;
171    private RootInfo mRoot;
172    private DocumentInfo mDocument;
173    private String mQuery = null;
174    // Save selection found during creation so it can be restored during directory loading.
175    private Selection mSelection = null;
176    private boolean mSearchMode = false;
177    private @Nullable ActionMode mActionMode;
178
179    @Override
180    public View onCreateView(
181            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
182        mInflater = inflater;
183        final View view = inflater.inflate(R.layout.fragment_directory, container, false);
184
185        mMessageBar = MessageBar.create(getChildFragmentManager());
186        mProgressBar = view.findViewById(R.id.progressbar);
187        mEmptyView = view.findViewById(android.R.id.empty);
188        mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
189        mRecView.setRecyclerListener(
190                new RecyclerListener() {
191                    @Override
192                    public void onViewRecycled(ViewHolder holder) {
193                        cancelThumbnailTask(holder.itemView);
194                    }
195                });
196
197        mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
198
199        // Make the recycler and the empty views responsive to drop events.
200        mRecView.setOnDragListener(mOnDragListener);
201        mEmptyView.setOnDragListener(mOnDragListener);
202
203        return view;
204    }
205
206    @Override
207    public void onDestroyView() {
208        mSelectionManager.clearSelection();
209
210        // Cancel any outstanding thumbnail requests
211        final int count = mRecView.getChildCount();
212        for (int i = 0; i < count; i++) {
213            final View view = mRecView.getChildAt(i);
214            cancelThumbnailTask(view);
215        }
216
217        super.onDestroyView();
218    }
219
220    @Override
221    public void onActivityCreated(Bundle savedInstanceState) {
222        super.onActivityCreated(savedInstanceState);
223
224        final Context context = getActivity();
225        final State state = getDisplayState();
226
227        // Read arguments when object created for the first time.
228        // Restore state if fragment recreated.
229        Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
230        mRoot = args.getParcelable(Shared.EXTRA_ROOT);
231        mDocument = args.getParcelable(Shared.EXTRA_DOC);
232        mStateKey = buildStateKey(mRoot, mDocument);
233        mQuery = args.getString(Shared.EXTRA_QUERY);
234        mType = args.getInt(Shared.EXTRA_TYPE);
235        final Selection selection = args.getParcelable(Shared.EXTRA_SELECTION);
236        mSelection = selection != null ? selection : new Selection();
237        mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
238
239        mIconHelper = new IconHelper(context, MODE_GRID);
240
241        mAdapter = new SectionBreakDocumentsAdapterWrapper(
242                this, new ModelBackedDocumentsAdapter(this, mIconHelper));
243
244        mRecView.setAdapter(mAdapter);
245
246        mLayout = new GridLayoutManager(getContext(), mColumnCount);
247        SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
248        if (lookup != null) {
249            mLayout.setSpanSizeLookup(lookup);
250        }
251        mRecView.setLayoutManager(mLayout);
252
253        mGestureDetector =
254                new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener());
255
256        mRecView.addOnItemTouchListener(mGestureDetector);
257
258        // TODO: instead of inserting the view into the constructor, extract listener-creation code
259        // and set the listener on the view after the fact.  Then the view doesn't need to be passed
260        // into the selection manager.
261        mSelectionManager = new MultiSelectManager(
262                mRecView,
263                mAdapter,
264                state.allowMultiple
265                    ? MultiSelectManager.MODE_MULTIPLE
266                    : MultiSelectManager.MODE_SINGLE,
267                null);
268
269        mSelectionManager.addCallback(new SelectionModeListener());
270
271        mModel = new Model();
272        mModel.addUpdateListener(mAdapter);
273        mModel.addUpdateListener(mModelUpdateListener);
274
275        // Make sure this is done after the RecyclerView is set up.
276        mFocusManager = new FocusManager(context, mRecView, mModel);
277
278        mTuner = FragmentTuner.pick(getContext(), state);
279        mClipper = new DocumentClipper(context);
280
281        final ActivityManager am = (ActivityManager) context.getSystemService(
282                Context.ACTIVITY_SERVICE);
283        boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
284        mIconHelper.setThumbnailsEnabled(!svelte);
285
286        // Kick off loader at least once
287        getLoaderManager().restartLoader(LOADER_ID, null, this);
288    }
289
290    @Override
291    public void onSaveInstanceState(Bundle outState) {
292        super.onSaveInstanceState(outState);
293
294        mSelectionManager.getSelection(mSelection);
295
296        outState.putInt(Shared.EXTRA_TYPE, mType);
297        outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
298        outState.putParcelable(Shared.EXTRA_DOC, mDocument);
299        outState.putString(Shared.EXTRA_QUERY, mQuery);
300
301        // Workaround. To avoid crash, write only up to 512 KB of selection.
302        // If more files are selected, then the selection will be lost.
303        final Parcel parcel = Parcel.obtain();
304        try {
305            mSelection.writeToParcel(parcel, 0);
306            if (parcel.dataSize() <= 512 * 1024) {
307                outState.putParcelable(Shared.EXTRA_SELECTION, mSelection);
308            }
309        } finally {
310            parcel.recycle();
311        }
312
313        outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
314    }
315
316    @Override
317    public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
318        switch (requestCode) {
319            case REQUEST_COPY_DESTINATION:
320                handleCopyResult(resultCode, data);
321                break;
322            default:
323                throw new UnsupportedOperationException("Unknown request code: " + requestCode);
324        }
325    }
326
327    private void handleCopyResult(int resultCode, Intent data) {
328        if (resultCode == Activity.RESULT_CANCELED || data == null) {
329            // User pressed the back button or otherwise cancelled the destination pick. Don't
330            // proceed with the copy.
331            return;
332        }
333
334        @OpType int operationType = data.getIntExtra(
335                FileOperationService.EXTRA_OPERATION,
336                FileOperationService.OPERATION_COPY);
337
338        FileOperations.start(
339                getActivity(),
340                getDisplayState().selectedDocumentsForCopy,
341                getDisplayState().stack.peek(),
342                (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
343                operationType);
344    }
345
346    protected boolean onDoubleTap(MotionEvent e) {
347        if (Events.isMouseEvent(e)) {
348            String id = getModelId(e);
349            if (id != null) {
350                return handleViewItem(id);
351            }
352        }
353        return false;
354    }
355
356    private boolean handleViewItem(String id) {
357        final Cursor cursor = mModel.getItem(id);
358
359        if (cursor == null) {
360            Log.w(TAG, "Can't view item. Can't obtain cursor for modeId" + id);
361            return false;
362        }
363
364        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
365        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
366        if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
367            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
368            ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
369            mSelectionManager.clearSelection();
370            return true;
371        }
372        return false;
373    }
374
375    @Override
376    public void onStop() {
377        super.onStop();
378
379        // Remember last scroll location
380        final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
381        getView().saveHierarchyState(container);
382        final State state = getDisplayState();
383        state.dirState.put(mStateKey, container);
384    }
385
386    public void onDisplayStateChanged() {
387        updateDisplayState();
388    }
389
390    public void onSortOrderChanged() {
391        // Sort order is implemented as a sorting wrapper around directory
392        // results. So when sort order changes, we force a reload of the directory.
393        getLoaderManager().restartLoader(LOADER_ID, null, this);
394    }
395
396    public void onViewModeChanged() {
397        // Mode change is just visual change; no need to kick loader.
398        updateDisplayState();
399    }
400
401    private void updateDisplayState() {
402        State state = getDisplayState();
403        updateLayout(state.derivedMode);
404        mRecView.setAdapter(mAdapter);
405    }
406
407    /**
408     * Updates the layout after the view mode switches.
409     * @param mode The new view mode.
410     */
411    private void updateLayout(@ViewMode int mode) {
412        mColumnCount = calculateColumnCount(mode);
413        if (mLayout != null) {
414            mLayout.setSpanCount(mColumnCount);
415        }
416
417        int pad = getDirectoryPadding(mode);
418        mRecView.setPadding(pad, pad, pad, pad);
419        mRecView.requestLayout();
420        mSelectionManager.handleLayoutChanged();  // RecyclerView doesn't do this for us
421        mIconHelper.setViewMode(mode);
422    }
423
424    private int calculateColumnCount(@ViewMode int mode) {
425        if (mode == MODE_LIST) {
426            // List mode is a "grid" with 1 column.
427            return 1;
428        }
429
430        int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
431        int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
432        int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
433
434        // RecyclerView sometimes gets a width of 0 (see b/27150284).  Clamp so that we always lay
435        // out the grid with at least 2 columns.
436        int columnCount = Math.max(2,
437                (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
438
439        return columnCount;
440    }
441
442    private int getDirectoryPadding(@ViewMode int mode) {
443        switch (mode) {
444            case MODE_GRID:
445                return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
446            case MODE_LIST:
447                return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
448            default:
449                throw new IllegalArgumentException("Unsupported layout mode: " + mode);
450        }
451    }
452
453    @Override
454    public int getColumnCount() {
455        return mColumnCount;
456    }
457
458    /**
459     * Manages the integration between our ActionMode and MultiSelectManager, initiating
460     * ActionMode when there is a selection, canceling it when there is no selection,
461     * and clearing selection when action mode is explicitly exited by the user.
462     */
463    private final class SelectionModeListener implements MultiSelectManager.Callback,
464            ActionMode.Callback, FragmentTuner.SelectionDetails {
465
466        private Selection mSelected = new Selection();
467
468        // Partial files are files that haven't been fully downloaded.
469        private int mPartialCount = 0;
470        private int mDirectoryCount = 0;
471        private int mNoDeleteCount = 0;
472        private int mNoRenameCount = 0;
473
474        private Menu mMenu;
475
476        @Override
477        public boolean onBeforeItemStateChange(String modelId, boolean selected) {
478            if (selected) {
479                final Cursor cursor = mModel.getItem(modelId);
480                if (cursor == null) {
481                    Log.w(TAG, "Can't obtain cursor for modelId: " + modelId);
482                    return false;
483                }
484
485                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
486                final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
487                if (!mTuner.canSelectType(docMimeType, docFlags)) {
488                    return false;
489                }
490
491                if (mSelected.size() >= MAX_DOCS_IN_INTENT) {
492                    Snackbars.makeSnackbar(
493                            getActivity(),
494                            R.string.too_many_selected,
495                            Snackbar.LENGTH_SHORT)
496                            .show();
497                    return false;
498                }
499            }
500            return true;
501        }
502
503        @Override
504        public void onItemStateChanged(String modelId, boolean selected) {
505            final Cursor cursor = mModel.getItem(modelId);
506            if (cursor == null) {
507                Log.w(TAG, "Model returned null cursor for document: " + modelId
508                        + ". Ignoring state changed event.");
509                return;
510            }
511
512            // TODO: Should this be happening in onSelectionChanged? Technically this callback is
513            // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
514            // selection changes here)
515            final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
516            if (MimePredicate.isDirectoryType(mimeType)) {
517                mDirectoryCount += selected ? 1 : -1;
518            }
519
520            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
521            if ((docFlags & Document.FLAG_PARTIAL) != 0) {
522                mPartialCount += selected ? 1 : -1;
523            }
524            if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
525                mNoDeleteCount += selected ? 1 : -1;
526            }
527            if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) {
528                mNoRenameCount += selected ? 1 : -1;
529            }
530        }
531
532        @Override
533        public void onSelectionChanged() {
534            mSelectionManager.getSelection(mSelected);
535            if (mSelected.size() > 0) {
536                if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
537                if (mActionMode == null) {
538                    if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
539                    mActionMode = getActivity().startActionMode(this);
540                }
541                updateActionMenu();
542            } else {
543                if (DEBUG) Log.d(TAG, "Finishing action mode.");
544                if (mActionMode != null) {
545                    mActionMode.finish();
546                }
547            }
548
549            if (mActionMode != null) {
550                assert(!mSelected.isEmpty());
551                final String title = Shared.getQuantityString(getActivity(),
552                        R.plurals.elements_selected, mSelected.size());
553                mActionMode.setTitle(title);
554                mRecView.announceForAccessibility(title);
555            }
556        }
557
558        // Called when the user exits the action mode
559        @Override
560        public void onDestroyActionMode(ActionMode mode) {
561            if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
562            mActionMode = null;
563            // clear selection
564            mSelectionManager.clearSelection();
565            mSelected.clear();
566
567            mDirectoryCount = 0;
568            mPartialCount = 0;
569            mNoDeleteCount = 0;
570            mNoRenameCount = 0;
571
572            // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
573            final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
574            toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
575
576            // This toolbar is not present in the fixed_layout
577            final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar);
578            if (rootsToolbar != null) {
579                rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
580            }
581        }
582
583        @Override
584        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
585            mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
586
587            int size = mSelectionManager.getSelection().size();
588            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
589            mode.setTitle(TextUtils.formatSelectedCount(size));
590
591            if (size > 0) {
592                // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
593                // these controls when using linear navigation.
594                final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
595                toolbar.setImportantForAccessibility(
596                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
597
598                // This toolbar is not present in the fixed_layout
599                final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
600                        R.id.roots_toolbar);
601                if (rootsToolbar != null) {
602                    rootsToolbar.setImportantForAccessibility(
603                            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
604                }
605                return true;
606            }
607
608            return false;
609        }
610
611        @Override
612        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
613            mMenu = menu;
614            updateActionMenu();
615            return true;
616        }
617
618        @Override
619        public boolean containsDirectories() {
620            return mDirectoryCount > 0;
621        }
622
623        @Override
624        public boolean containsPartialFiles() {
625            return mPartialCount > 0;
626        }
627
628        @Override
629        public boolean canDelete() {
630            return mNoDeleteCount == 0;
631        }
632
633        @Override
634        public boolean canRename() {
635            return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
636        }
637
638        private void updateActionMenu() {
639            assert(mMenu != null);
640            mTuner.updateActionMenu(mMenu, this);
641            Menus.disableHiddenItems(mMenu);
642        }
643
644        @Override
645        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
646            Selection selection = mSelectionManager.getSelection(new Selection());
647
648            switch (item.getItemId()) {
649                case R.id.menu_open:
650                    openDocuments(selection);
651                    mode.finish();
652                    return true;
653
654                case R.id.menu_share:
655                    shareDocuments(selection);
656                    // TODO: Only finish selection if share action is completed.
657                    mode.finish();
658                    return true;
659
660                case R.id.menu_delete:
661                    // deleteDocuments will end action mode if the documents are deleted.
662                    // It won't end action mode if user cancels the delete.
663                    deleteDocuments(selection);
664                    return true;
665
666                case R.id.menu_copy_to:
667                    transferDocuments(selection, FileOperationService.OPERATION_COPY);
668                    // TODO: Only finish selection mode if copy-to is not canceled.
669                    // Need to plum down into handling the way we do with deleteDocuments.
670                    mode.finish();
671                    return true;
672
673                case R.id.menu_move_to:
674                    // Exit selection mode first, so we avoid deselecting deleted documents.
675                    mode.finish();
676                    transferDocuments(selection, FileOperationService.OPERATION_MOVE);
677                    return true;
678
679                case R.id.menu_copy_to_clipboard:
680                    copySelectedToClipboard();
681                    return true;
682
683                case R.id.menu_select_all:
684                    selectAllFiles();
685                    return true;
686
687                case R.id.menu_rename:
688                    // Exit selection mode first, so we avoid deselecting deleted
689                    // (renamed) documents.
690                    mode.finish();
691                    renameDocuments(selection);
692                    return true;
693
694                default:
695                    if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
696                    return false;
697            }
698        }
699    }
700
701    public final boolean onBackPressed() {
702        if (mSelectionManager.hasSelection()) {
703            if (DEBUG) Log.d(TAG, "Clearing selection on selection manager.");
704            mSelectionManager.clearSelection();
705            return true;
706        }
707        return false;
708    }
709
710    private void cancelThumbnailTask(View view) {
711        final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
712        if (iconThumb != null) {
713            mIconHelper.stopLoading(iconThumb);
714        }
715    }
716
717    private void openDocuments(final Selection selected) {
718        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);
719
720        new GetDocumentsTask() {
721            @Override
722            void onDocumentsReady(List<DocumentInfo> docs) {
723                // TODO: Implement support in Files activity for opening multiple docs.
724                BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
725            }
726        }.execute(selected);
727    }
728
729    private void shareDocuments(final Selection selected) {
730        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
731
732        new GetDocumentsTask() {
733            @Override
734            void onDocumentsReady(List<DocumentInfo> docs) {
735                Intent intent;
736
737                // Filter out directories and virtual files - those can't be shared.
738                List<DocumentInfo> docsForSend = new ArrayList<>();
739                for (DocumentInfo doc: docs) {
740                    if (!doc.isDirectory() && !doc.isVirtualDocument()) {
741                        docsForSend.add(doc);
742                    }
743                }
744
745                if (docsForSend.size() == 1) {
746                    final DocumentInfo doc = docsForSend.get(0);
747
748                    intent = new Intent(Intent.ACTION_SEND);
749                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
750                    intent.addCategory(Intent.CATEGORY_DEFAULT);
751                    intent.setType(doc.mimeType);
752                    intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
753
754                } else if (docsForSend.size() > 1) {
755                    intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
756                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
757                    intent.addCategory(Intent.CATEGORY_DEFAULT);
758
759                    final ArrayList<String> mimeTypes = new ArrayList<>();
760                    final ArrayList<Uri> uris = new ArrayList<>();
761                    for (DocumentInfo doc : docsForSend) {
762                        mimeTypes.add(doc.mimeType);
763                        uris.add(doc.derivedUri);
764                    }
765
766                    intent.setType(findCommonMimeType(mimeTypes));
767                    intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
768
769                } else {
770                    return;
771                }
772
773                intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
774                startActivity(intent);
775            }
776        }.execute(selected);
777    }
778
779    private String generateDeleteMessage(final List<DocumentInfo> docs) {
780        String message;
781        int dirsCount = 0;
782
783        for (DocumentInfo doc : docs) {
784            if (doc.isDirectory()) {
785                ++dirsCount;
786            }
787        }
788
789        if (docs.size() == 1) {
790            // Deleteing 1 file xor 1 folder in cwd
791
792            // Address b/28772371, where including user strings in message can result in
793            // broken bidirectional support.
794            String displayName = BidiFormatter.getInstance().unicodeWrap(docs.get(0).displayName);
795            message = dirsCount == 0
796                    ? getActivity().getString(R.string.delete_filename_confirmation_message,
797                            displayName)
798                    : getActivity().getString(R.string.delete_foldername_confirmation_message,
799                            displayName);
800        } else if (dirsCount == 0) {
801            // Deleting only files in cwd
802            message = Shared.getQuantityString(getActivity(),
803                    R.plurals.delete_files_confirmation_message, docs.size());
804        } else if (dirsCount == docs.size()) {
805            // Deleting only folders in cwd
806            message = Shared.getQuantityString(getActivity(),
807                    R.plurals.delete_folders_confirmation_message, docs.size());
808        } else {
809            // Deleting mixed items (files and folders) in cwd
810            message = Shared.getQuantityString(getActivity(),
811                    R.plurals.delete_items_confirmation_message, docs.size());
812        }
813        return message;
814    }
815
816    private void deleteDocuments(final Selection selected) {
817        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_DELETE);
818
819        assert(!selected.isEmpty());
820
821        final DocumentInfo srcParent = getDisplayState().stack.peek();
822        new GetDocumentsTask() {
823            @Override
824            void onDocumentsReady(final List<DocumentInfo> docs) {
825
826                TextView message =
827                        (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
828                message.setText(generateDeleteMessage(docs));
829
830                // This "insta-hides" files that are being deleted, because
831                // the delete operation may be not execute immediately (it
832                // may be queued up on the FileOperationService.)
833                // To hide the files locally, we call the hide method on the adapter
834                // ...which a live object...cannot be parceled.
835                // For that reason, for now, we implement this dialog NOT
836                // as a fragment (which can survive rotation and have its own state),
837                // but as a simple runtime dialog. So rotating a device with an
838                // active delete dialog...results in that dialog disappearing.
839                // We can do better, but don't have cycles for it now.
840                new AlertDialog.Builder(getActivity())
841                    .setView(message)
842                    .setPositiveButton(
843                         android.R.string.yes,
844                         new DialogInterface.OnClickListener() {
845                            public void onClick(DialogInterface dialog, int id) {
846                                // Finish selection mode first which clears selection so we
847                                // don't end up trying to deselect deleted documents.
848                                // This is done here, rather in the onActionItemClicked
849                                // so we can avoid de-selecting items in the case where
850                                // the user cancels the delete.
851                                if (mActionMode != null) {
852                                    mActionMode.finish();
853                                } else {
854                                    Log.w(TAG, "Action mode is null before deleting documents.");
855                                }
856                                // Hide the files in the UI...since the operation
857                                // might be queued up on FileOperationService.
858                                // We're walking a line here.
859                                mAdapter.hide(selected.getAll());
860                                FileOperations.delete(
861                                        getActivity(), docs, srcParent, getDisplayState().stack);
862                            }
863                        })
864                    .setNegativeButton(android.R.string.no, null)
865                    .show();
866            }
867        }.execute(selected);
868    }
869
870    private void transferDocuments(final Selection selected, final @OpType int mode) {
871        if(mode == FileOperationService.OPERATION_COPY) {
872            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_TO);
873        } else if (mode == FileOperationService.OPERATION_MOVE) {
874            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_MOVE_TO);
875        }
876
877        // Pop up a dialog to pick a destination.  This is inadequate but works for now.
878        // TODO: Implement a picker that is to spec.
879        final Intent intent = new Intent(
880                Shared.ACTION_PICK_COPY_DESTINATION,
881                Uri.EMPTY,
882                getActivity(),
883                DocumentsActivity.class);
884
885
886        // Relay any config overrides bits present in the original intent.
887        Intent original = getActivity().getIntent();
888        if (original != null && original.hasExtra(Shared.EXTRA_PRODUCTIVITY_MODE)) {
889            intent.putExtra(
890                    Shared.EXTRA_PRODUCTIVITY_MODE,
891                    original.getBooleanExtra(Shared.EXTRA_PRODUCTIVITY_MODE, false));
892        }
893
894        // Set an appropriate title on the drawer when it is shown in the picker.
895        // Coupled with the fact that we auto-open the drawer for copy/move operations
896        // it should basically be the thing people see first.
897        int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
898                ? R.string.menu_move : R.string.menu_copy;
899        intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
900
901        new GetDocumentsTask() {
902            @Override
903            void onDocumentsReady(List<DocumentInfo> docs) {
904                // TODO: Can this move to Fragment bundle state?
905                getDisplayState().selectedDocumentsForCopy = docs;
906
907                // Determine if there is a directory in the set of documents
908                // to be copied? Why? Directory creation isn't supported by some roots
909                // (like Downloads). This informs DocumentsActivity (the "picker")
910                // to restrict available roots to just those with support.
911                intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
912                intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
913
914                // This just identifies the type of request...we'll check it
915                // when we reveive a response.
916                startActivityForResult(intent, REQUEST_COPY_DESTINATION);
917            }
918
919        }.execute(selected);
920    }
921
922    private static boolean hasDirectory(List<DocumentInfo> docs) {
923        for (DocumentInfo info : docs) {
924            if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
925                return true;
926            }
927        }
928        return false;
929    }
930
931    private void renameDocuments(Selection selected) {
932        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_RENAME);
933
934        // Batch renaming not supported
935        // Rename option is only available in menu when 1 document selected
936        assert(selected.size() == 1);
937
938        new GetDocumentsTask() {
939            @Override
940            void onDocumentsReady(List<DocumentInfo> docs) {
941                RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
942            }
943        }.execute(selected);
944    }
945
946    @Override
947    public void initDocumentHolder(DocumentHolder holder) {
948        holder.addEventListener(mItemEventListener);
949        holder.itemView.setOnFocusChangeListener(mFocusManager);
950    }
951
952    @Override
953    public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
954        setupDragAndDropOnDocumentView(holder.itemView, cursor);
955    }
956
957    @Override
958    public State getDisplayState() {
959        return ((BaseActivity) getActivity()).getDisplayState();
960    }
961
962    @Override
963    public Model getModel() {
964        return mModel;
965    }
966
967    @Override
968    public boolean isDocumentEnabled(String docMimeType, int docFlags) {
969        return mTuner.isDocumentEnabled(docMimeType, docFlags);
970    }
971
972    private void showEmptyDirectory() {
973        showEmptyView(R.string.empty, R.drawable.cabinet);
974    }
975
976    private void showNoResults(RootInfo root) {
977        CharSequence msg = getContext().getResources().getText(R.string.no_results);
978        showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
979    }
980
981    private void showQueryError() {
982        showEmptyView(R.string.query_error, R.drawable.hourglass);
983    }
984
985    private void showEmptyView(@StringRes int id, int drawable) {
986        showEmptyView(getContext().getResources().getText(id), drawable);
987    }
988
989    private void showEmptyView(CharSequence msg, int drawable) {
990        View content = mEmptyView.findViewById(R.id.content);
991        TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
992        ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
993        msgView.setText(msg);
994        imageView.setImageResource(drawable);
995
996        mEmptyView.setVisibility(View.VISIBLE);
997        mEmptyView.requestFocus();
998        mRecView.setVisibility(View.GONE);
999    }
1000
1001    private void showDirectory() {
1002        mEmptyView.setVisibility(View.GONE);
1003        mRecView.setVisibility(View.VISIBLE);
1004        mRecView.requestFocus();
1005    }
1006
1007    private String findCommonMimeType(List<String> mimeTypes) {
1008        String[] commonType = mimeTypes.get(0).split("/");
1009        if (commonType.length != 2) {
1010            return "*/*";
1011        }
1012
1013        for (int i = 1; i < mimeTypes.size(); i++) {
1014            String[] type = mimeTypes.get(i).split("/");
1015            if (type.length != 2) continue;
1016
1017            if (!commonType[1].equals(type[1])) {
1018                commonType[1] = "*";
1019            }
1020
1021            if (!commonType[0].equals(type[0])) {
1022                commonType[0] = "*";
1023                commonType[1] = "*";
1024                break;
1025            }
1026        }
1027
1028        return commonType[0] + "/" + commonType[1];
1029    }
1030
1031    private void copyFromClipboard() {
1032        new AsyncTask<Void, Void, List<DocumentInfo>>() {
1033
1034            @Override
1035            protected List<DocumentInfo> doInBackground(Void... params) {
1036                return mClipper.getClippedDocuments();
1037            }
1038
1039            @Override
1040            protected void onPostExecute(List<DocumentInfo> docs) {
1041                DocumentInfo destination =
1042                        ((BaseActivity) getActivity()).getCurrentDirectory();
1043                copyDocuments(docs, destination);
1044            }
1045        }.execute();
1046    }
1047
1048    private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
1049        assert(clipData != null);
1050
1051        new AsyncTask<Void, Void, List<DocumentInfo>>() {
1052
1053            @Override
1054            protected List<DocumentInfo> doInBackground(Void... params) {
1055                return mClipper.getDocumentsFromClipData(clipData);
1056            }
1057
1058            @Override
1059            protected void onPostExecute(List<DocumentInfo> docs) {
1060                copyDocuments(docs, destination);
1061            }
1062        }.execute();
1063    }
1064
1065    private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
1066        BaseActivity activity = (BaseActivity) getActivity();
1067        if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
1068            Snackbars.makeSnackbar(
1069                    getActivity(),
1070                    R.string.clipboard_files_cannot_paste,
1071                    Snackbar.LENGTH_SHORT)
1072                    .show();
1073            return;
1074        }
1075
1076        if (docs.isEmpty()) {
1077            return;
1078        }
1079
1080        final DocumentStack curStack = getDisplayState().stack;
1081        DocumentStack tmpStack = new DocumentStack();
1082        if (destination != null) {
1083            tmpStack.push(destination);
1084            tmpStack.addAll(curStack);
1085        } else {
1086            tmpStack = curStack;
1087        }
1088
1089        FileOperations.copy(getActivity(), docs, tmpStack);
1090    }
1091
1092    public void copySelectedToClipboard() {
1093        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
1094
1095        Selection selection = mSelectionManager.getSelection(new Selection());
1096        if (!selection.isEmpty()) {
1097            copySelectionToClipboard(selection);
1098            mSelectionManager.clearSelection();
1099        }
1100    }
1101
1102    void copySelectionToClipboard(Selection selection) {
1103        assert(!selection.isEmpty());
1104        new GetDocumentsTask() {
1105            @Override
1106            void onDocumentsReady(List<DocumentInfo> docs) {
1107                mClipper.clipDocuments(docs);
1108                Activity activity = getActivity();
1109                Snackbars.makeSnackbar(activity,
1110                        activity.getResources().getQuantityString(
1111                                R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
1112                        Snackbar.LENGTH_SHORT).show();
1113            }
1114        }.execute(selection);
1115    }
1116
1117    public void pasteFromClipboard() {
1118        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
1119
1120        copyFromClipboard();
1121        getActivity().invalidateOptionsMenu();
1122    }
1123
1124    /**
1125     * Returns true if the list of files can be copied to destination. Note that this
1126     * is a policy check only. Currently the method does not attempt to verify
1127     * available space or any other environmental aspects possibly resulting in
1128     * failure to copy.
1129     *
1130     * @return true if the list of files can be copied to destination.
1131     */
1132    private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
1133        if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
1134            return false;
1135        }
1136
1137        // Can't copy folders to downloads, because we don't show folders there.
1138        if (root.isDownloads()) {
1139            for (DocumentInfo docs : files) {
1140                if (docs.isDirectory()) {
1141                    return false;
1142                }
1143            }
1144        }
1145
1146        return true;
1147    }
1148
1149    public void selectAllFiles() {
1150        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SELECT_ALL);
1151
1152        // Exclude disabled files.
1153        Set<String> enabled = new HashSet<String>();
1154        List<String> modelIds = mAdapter.getModelIds();
1155
1156        // Get the current selection.
1157        String[] alreadySelected = mSelectionManager.getSelection().getAll();
1158        for (String id : alreadySelected) {
1159           enabled.add(id);
1160        }
1161
1162        for (String id : modelIds) {
1163            Cursor cursor = getModel().getItem(id);
1164            if (cursor == null) {
1165                Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
1166                continue;
1167            }
1168            String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1169            int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
1170            if (mTuner.canSelectType(docMimeType, docFlags)) {
1171                if (enabled.size() >= MAX_DOCS_IN_INTENT) {
1172                    Snackbars.makeSnackbar(
1173                        getActivity(),
1174                        R.string.too_many_in_select_all,
1175                        Snackbar.LENGTH_SHORT)
1176                        .show();
1177                    break;
1178                }
1179                enabled.add(id);
1180            }
1181        }
1182
1183        // Only select things currently visible in the adapter.
1184        boolean changed = mSelectionManager.setItemsSelected(enabled, true);
1185        if (changed) {
1186            updateDisplayState();
1187        }
1188    }
1189
1190    /**
1191     * Attempts to restore focus on the directory listing.
1192     */
1193    public void requestFocus() {
1194        mFocusManager.restoreLastFocus();
1195    }
1196
1197    private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
1198        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1199        if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
1200            // Make a directory item a drop target. Drop on non-directories and empty space
1201            // is handled at the list/grid view level.
1202            view.setOnDragListener(mOnDragListener);
1203        }
1204
1205        if (mTuner.dragAndDropEnabled()) {
1206            // Make all items draggable.
1207            view.setOnLongClickListener(onLongClickListener);
1208        }
1209    }
1210
1211    private View.OnDragListener mOnDragListener = new View.OnDragListener() {
1212        @Override
1213        public boolean onDrag(View v, DragEvent event) {
1214            switch (event.getAction()) {
1215                case DragEvent.ACTION_DRAG_STARTED:
1216                    // TODO: Check if the event contains droppable data.
1217                    return true;
1218
1219                // TODO: Expand drop target directory on hover?
1220                case DragEvent.ACTION_DRAG_ENTERED:
1221                    setDropTargetHighlight(v, true);
1222                    return true;
1223                case DragEvent.ACTION_DRAG_EXITED:
1224                    setDropTargetHighlight(v, false);
1225                    return true;
1226
1227                case DragEvent.ACTION_DRAG_LOCATION:
1228                    return true;
1229
1230                case DragEvent.ACTION_DRAG_ENDED:
1231                    if (event.getResult()) {
1232                        // Exit selection mode if the drop was handled.
1233                        mSelectionManager.clearSelection();
1234                    }
1235                    return true;
1236
1237                case DragEvent.ACTION_DROP:
1238                    // After a drop event, always stop highlighting the target.
1239                    setDropTargetHighlight(v, false);
1240
1241                    ClipData clipData = event.getClipData();
1242                    if (clipData == null) {
1243                        Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
1244                        return false;
1245                    }
1246
1247                    // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
1248                    // multi-window drag, because localState isn't carried over from one process to
1249                    // another.
1250                    Object src = event.getLocalState();
1251                    DocumentInfo dst = getDestination(v);
1252                    if (Objects.equals(src, dst)) {
1253                        if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
1254                        return false;
1255                    }
1256
1257                    // Recognize multi-window drag and drop based on the fact that localState is not
1258                    // carried between processes. It will stop working when the localsState behavior
1259                    // is changed. The info about window should be passed in the localState then.
1260                    // The localState could also be null for copying from Recents in single window
1261                    // mode, but Recents doesn't offer this functionality (no directories).
1262                    Metrics.logUserAction(getContext(),
1263                            src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
1264                                    : Metrics.USER_ACTION_DRAG_N_DROP);
1265
1266                    copyFromClipData(clipData, dst);
1267                    return true;
1268            }
1269            return false;
1270        }
1271
1272        private DocumentInfo getDestination(View v) {
1273            String id = getModelId(v);
1274            if (id != null) {
1275                Cursor dstCursor = mModel.getItem(id);
1276                if (dstCursor == null) {
1277                    Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
1278                    return null;
1279                }
1280                return DocumentInfo.fromDirectoryCursor(dstCursor);
1281            }
1282
1283            if (v == mRecView || v == mEmptyView) {
1284                return getDisplayState().stack.peek();
1285            }
1286
1287            return null;
1288        }
1289
1290        private void setDropTargetHighlight(View v, boolean highlight) {
1291            // Note: use exact comparison - this code is searching for views which are children of
1292            // the RecyclerView instance in the UI.
1293            if (v.getParent() == mRecView) {
1294                RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
1295                if (vh instanceof DocumentHolder) {
1296                    ((DocumentHolder) vh).setHighlighted(highlight);
1297                }
1298            }
1299        }
1300    };
1301
1302    /**
1303     * Gets the model ID for a given motion event (using the event position)
1304     */
1305    private String getModelId(MotionEvent e) {
1306        View view = mRecView.findChildViewUnder(e.getX(), e.getY());
1307        if (view == null) {
1308            return null;
1309        }
1310        RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(view);
1311        if (vh instanceof DocumentHolder) {
1312            return ((DocumentHolder) vh).modelId;
1313        } else {
1314            return null;
1315        }
1316    }
1317
1318    /**
1319     * Gets the model ID for a given RecyclerView item.
1320     * @param view A View that is a document item view, or a child of a document item view.
1321     * @return The Model ID for the given document, or null if the given view is not associated with
1322     *     a document item view.
1323     */
1324    private String getModelId(View view) {
1325        View itemView = mRecView.findContainingItemView(view);
1326        if (itemView != null) {
1327            RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
1328            if (vh instanceof DocumentHolder) {
1329                return ((DocumentHolder) vh).modelId;
1330            }
1331        }
1332        return null;
1333    }
1334
1335    private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
1336        String modelId = getModelId(currentItemView);
1337        if (modelId == null) {
1338            return Collections.EMPTY_LIST;
1339        }
1340
1341        final List<DocumentInfo> selectedDocs =
1342                mModel.getDocuments(mSelectionManager.getSelection());
1343        if (!selectedDocs.isEmpty()) {
1344            if (!isSelected(modelId)) {
1345                // There is a selection that does not include the current item, drag nothing.
1346                return Collections.EMPTY_LIST;
1347            }
1348            return selectedDocs;
1349        }
1350
1351        final Cursor cursor = mModel.getItem(modelId);
1352        if (cursor == null) {
1353            Log.w(TAG, "Undraggable document. Can't obtain cursor for modelId " + modelId);
1354            return Collections.EMPTY_LIST;
1355        }
1356
1357        return Lists.newArrayList(
1358                DocumentInfo.fromDirectoryCursor(cursor));
1359    }
1360
1361    private static class DragShadowBuilder extends View.DragShadowBuilder {
1362
1363        private final Context mContext;
1364        private final IconHelper mIconHelper;
1365        private final LayoutInflater mInflater;
1366        private final View mShadowView;
1367        private final TextView mTitle;
1368        private final ImageView mIcon;
1369        private final int mWidth;
1370        private final int mHeight;
1371
1372        public DragShadowBuilder(Context context, IconHelper iconHelper, List<DocumentInfo> docs) {
1373            mContext = context;
1374            mIconHelper = iconHelper;
1375            mInflater = LayoutInflater.from(context);
1376
1377            mWidth = mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
1378            mHeight= mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_height);
1379
1380            mShadowView = mInflater.inflate(R.layout.drag_shadow_layout, null);
1381            mTitle = (TextView) mShadowView.findViewById(android.R.id.title);
1382            mIcon = (ImageView) mShadowView.findViewById(android.R.id.icon);
1383
1384            mTitle.setText(getTitle(docs));
1385            mIcon.setImageDrawable(getIcon(docs));
1386        }
1387
1388        private Drawable getIcon(List<DocumentInfo> docs) {
1389            if (docs.size() == 1) {
1390                final DocumentInfo doc = docs.get(0);
1391                return mIconHelper.getDocumentIcon(mContext, doc.authority, doc.documentId,
1392                        doc.mimeType, doc.icon);
1393            }
1394            return mContext.getDrawable(R.drawable.ic_doc_generic);
1395        }
1396
1397        private String getTitle(List<DocumentInfo> docs) {
1398            if (docs.size() == 1) {
1399                final DocumentInfo doc = docs.get(0);
1400                return doc.displayName;
1401            }
1402            return Shared.getQuantityString(mContext, R.plurals.elements_dragged, docs.size());
1403        }
1404
1405        @Override
1406        public void onProvideShadowMetrics(
1407                Point shadowSize, Point shadowTouchPoint) {
1408            shadowSize.set(mWidth, mHeight);
1409            shadowTouchPoint.set(mWidth, mHeight);
1410        }
1411
1412        @Override
1413        public void onDrawShadow(Canvas canvas) {
1414            Rect r = canvas.getClipBounds();
1415            // Calling measure is necessary in order for all child views to get correctly laid out.
1416            mShadowView.measure(
1417                    View.MeasureSpec.makeMeasureSpec(r.right- r.left, View.MeasureSpec.EXACTLY),
1418                    View.MeasureSpec.makeMeasureSpec(r.top- r.bottom, View.MeasureSpec.EXACTLY));
1419            mShadowView.layout(r.left, r.top, r.right, r.bottom);
1420            mShadowView.draw(canvas);
1421        }
1422    }
1423    /**
1424     * Abstract task providing support for loading documents *off*
1425     * the main thread. And if it isn't obvious, creating a list
1426     * of documents (especially large lists) can be pretty expensive.
1427     */
1428    private abstract class GetDocumentsTask
1429            extends AsyncTask<Selection, Void, List<DocumentInfo>> {
1430        @Override
1431        protected final List<DocumentInfo> doInBackground(Selection... selected) {
1432            return mModel.getDocuments(selected[0]);
1433        }
1434
1435        @Override
1436        protected final void onPostExecute(List<DocumentInfo> docs) {
1437            onDocumentsReady(docs);
1438        }
1439
1440        abstract void onDocumentsReady(List<DocumentInfo> docs);
1441    }
1442
1443    @Override
1444    public boolean isSelected(String modelId) {
1445        return mSelectionManager.getSelection().contains(modelId);
1446    }
1447
1448    private class ItemEventListener implements DocumentHolder.EventListener {
1449        @Override
1450        public boolean onActivate(DocumentHolder doc) {
1451            // Toggle selection if we're in selection mode, othewise, view item.
1452            if (mSelectionManager.hasSelection()) {
1453                mSelectionManager.toggleSelection(doc.modelId);
1454            } else {
1455                handleViewItem(doc.modelId);
1456            }
1457            return true;
1458        }
1459
1460        @Override
1461        public boolean onSelect(DocumentHolder doc) {
1462            mSelectionManager.toggleSelection(doc.modelId);
1463            mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
1464            return true;
1465        }
1466
1467        @Override
1468        public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
1469            // Only handle key-down events. This is simpler, consistent with most other UIs, and
1470            // enables the handling of repeated key events from holding down a key.
1471            if (event.getAction() != KeyEvent.ACTION_DOWN) {
1472                return false;
1473            }
1474
1475            // Ignore tab key events.  Those should be handled by the top-level key handler.
1476            if (keyCode == KeyEvent.KEYCODE_TAB) {
1477                return false;
1478            }
1479
1480            if (mFocusManager.handleKey(doc, keyCode, event)) {
1481                // Handle range selection adjustments. Extending the selection will adjust the
1482                // bounds of the in-progress range selection. Each time an unshifted navigation
1483                // event is received, the range selection is restarted.
1484                if (shouldExtendSelection(doc, event)) {
1485                    if (!mSelectionManager.isRangeSelectionActive()) {
1486                        // Start a range selection if one isn't active
1487                        mSelectionManager.startRangeSelection(doc.getAdapterPosition());
1488                    }
1489                    mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
1490                } else {
1491                    mSelectionManager.endRangeSelection();
1492                }
1493                return true;
1494            }
1495
1496            // Handle enter key events
1497            switch (keyCode) {
1498                case KeyEvent.KEYCODE_ENTER:
1499                    if (event.isShiftPressed()) {
1500                        return onSelect(doc);
1501                    }
1502                    // For non-shifted enter keypresses, fall through.
1503                case KeyEvent.KEYCODE_DPAD_CENTER:
1504                case KeyEvent.KEYCODE_BUTTON_A:
1505                    return onActivate(doc);
1506                case KeyEvent.KEYCODE_FORWARD_DEL:
1507                    // This has to be handled here instead of in a keyboard shortcut, because
1508                    // keyboard shortcuts all have to be modified with the 'Ctrl' key.
1509                    if (mSelectionManager.hasSelection()) {
1510                        Selection selection = mSelectionManager.getSelection(new Selection());
1511                        deleteDocuments(selection);
1512                    }
1513                    // Always handle the key, even if there was nothing to delete. This is a
1514                    // precaution to prevent other handlers from potentially picking up the event
1515                    // and triggering extra behaviours.
1516                    return true;
1517            }
1518
1519            return false;
1520        }
1521
1522        private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) {
1523            if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
1524                return false;
1525            }
1526
1527            // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost
1528            // the same, and responsible for the same thing (whether to select or not).
1529            final Cursor cursor = mModel.getItem(doc.modelId);
1530            if (cursor == null) {
1531                Log.w(TAG, "Couldn't obtain cursor for modelId: " + doc.modelId);
1532                return false;
1533            }
1534
1535            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
1536            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
1537            return mTuner.canSelectType(docMimeType, docFlags);
1538        }
1539    }
1540
1541    private final class ModelUpdateListener implements Model.UpdateListener {
1542        @Override
1543        public void onModelUpdate(Model model) {
1544            if (model.info != null || model.error != null) {
1545                mMessageBar.setInfo(model.info);
1546                mMessageBar.setError(model.error);
1547                mMessageBar.show();
1548            }
1549
1550            mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
1551
1552            if (model.isEmpty()) {
1553                if (mSearchMode) {
1554                    showNoResults(getDisplayState().stack.root);
1555                } else {
1556                    showEmptyDirectory();
1557                }
1558            } else {
1559                showDirectory();
1560                mAdapter.notifyDataSetChanged();
1561            }
1562
1563            if (!model.isLoading()) {
1564                ((BaseActivity) getActivity()).notifyDirectoryLoaded(
1565                    model.doc != null ? model.doc.derivedUri : null);
1566            }
1567        }
1568
1569        @Override
1570        public void onModelUpdateFailed(Exception e) {
1571            showQueryError();
1572        }
1573    }
1574
1575    private DragStartHelper.OnDragStartListener mOnDragStartListener =
1576            new DragStartHelper.OnDragStartListener() {
1577        @Override
1578        public boolean onDragStart(View v, DragStartHelper helper) {
1579            if (isSelected(getModelId(v))) {
1580                List<DocumentInfo> docs = getDraggableDocuments(v);
1581                if (docs.isEmpty()) {
1582                    return false;
1583                }
1584                v.startDragAndDrop(
1585                        mClipper.getClipDataForDocuments(docs),
1586                        new DragShadowBuilder(getActivity(), mIconHelper, docs),
1587                        getDisplayState().stack.peek(),
1588                        View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
1589                                View.DRAG_FLAG_GLOBAL_URI_WRITE
1590                );
1591                return true;
1592            }
1593
1594            return false;
1595        }
1596    };
1597
1598    private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
1599
1600    private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
1601        @Override
1602        public boolean onLongClick(View v) {
1603            return mDragHelper.onLongClick(v);
1604        }
1605    };
1606
1607    // Previously we listened to events with one class, only to bounce them forward
1608    // to GestureDetector. We're still doing that here, but with a single class
1609    // that reduces overall complexity in our glue code.
1610    private static final class ListeningGestureDetector extends GestureDetector
1611            implements OnItemTouchListener {
1612
1613        private int mLastTool = -1;
1614        private DragStartHelper mDragHelper;
1615
1616        public ListeningGestureDetector(
1617                Context context, DragStartHelper dragHelper, GestureListener listener) {
1618            super(context, listener);
1619            mDragHelper = dragHelper;
1620            setOnDoubleTapListener(listener);
1621        }
1622
1623        boolean mouseSpawnedLastEvent() {
1624            return Events.isMouseType(mLastTool);
1625        }
1626
1627        boolean touchSpawnedLastEvent() {
1628            return Events.isTouchType(mLastTool);
1629        }
1630
1631        @Override
1632        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
1633            mLastTool = e.getToolType(0);
1634
1635            // Detect drag events. When a drag is detected, intercept the rest of the gesture.
1636            View itemView = rv.findChildViewUnder(e.getX(), e.getY());
1637            if (itemView != null && mDragHelper.onTouch(itemView,  e)) {
1638                return true;
1639            }
1640            // Forward unhandled events to the GestureDetector.
1641            onTouchEvent(e);
1642
1643            return false;
1644        }
1645
1646        @Override
1647        public void onTouchEvent(RecyclerView rv, MotionEvent e) {
1648            View itemView = rv.findChildViewUnder(e.getX(), e.getY());
1649            mDragHelper.onTouch(itemView,  e);
1650            // Note: even though this event is being handled as part of a drag gesture, continue
1651            // forwarding to the GestureDetector. The detector needs to see the entire cluster of
1652            // events in order to properly interpret gestures.
1653            onTouchEvent(e);
1654        }
1655
1656        @Override
1657        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
1658    }
1659
1660    /**
1661     * The gesture listener for items in the list/grid view. Interprets gestures and sends the
1662     * events to the target DocumentHolder, whence they are routed to the appropriate listener.
1663     */
1664    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
1665        @Override
1666        public boolean onSingleTapUp(MotionEvent e) {
1667            // Single tap logic:
1668            // If the selection manager is active, it gets first whack at handling tap
1669            // events. Otherwise, tap events are routed to the target DocumentHolder.
1670            boolean handled = mSelectionManager.onSingleTapUp(
1671                        new MotionInputEvent(e, mRecView));
1672
1673            if (handled) {
1674                return handled;
1675            }
1676
1677            // Give the DocumentHolder a crack at the event.
1678            DocumentHolder holder = getTarget(e);
1679            if (holder != null) {
1680                handled = holder.onSingleTapUp(e);
1681            }
1682
1683            return handled;
1684        }
1685
1686        @Override
1687        public void onLongPress(MotionEvent e) {
1688            // Long-press events get routed directly to the selection manager. They can be
1689            // changed to route through the DocumentHolder if necessary.
1690            mSelectionManager.onLongPress(new MotionInputEvent(e, mRecView));
1691        }
1692
1693        @Override
1694        public boolean onDoubleTap(MotionEvent e) {
1695            // Double-tap events are handled directly by the DirectoryFragment. They can be changed
1696            // to route through the DocumentHolder if necessary.
1697            return DirectoryFragment.this.onDoubleTap(e);
1698        }
1699
1700        private @Nullable DocumentHolder getTarget(MotionEvent e) {
1701            View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
1702            if (childView != null) {
1703                return (DocumentHolder) mRecView.getChildViewHolder(childView);
1704            } else {
1705                return null;
1706            }
1707        }
1708    }
1709
1710    public static void showDirectory(
1711            FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
1712        create(fm, TYPE_NORMAL, root, doc, null, anim);
1713    }
1714
1715    public static void showRecentsOpen(FragmentManager fm, int anim) {
1716        create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
1717    }
1718
1719    public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
1720            String query) {
1721        DirectoryFragment df = get(fm);
1722
1723        df.mQuery = query;
1724        df.mRoot = root;
1725        df.mDocument = doc;
1726        df.mSearchMode =  query != null;
1727        df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1728    }
1729
1730    public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1731            String query) {
1732        DirectoryFragment df = get(fm);
1733        df.mType = type;
1734        df.mQuery = query;
1735        df.mRoot = root;
1736        df.mDocument = doc;
1737        df.mSearchMode =  query != null;
1738        df.getLoaderManager().restartLoader(LOADER_ID, null, df);
1739    }
1740
1741    public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
1742            String query, int anim) {
1743        final Bundle args = new Bundle();
1744        args.putInt(Shared.EXTRA_TYPE, type);
1745        args.putParcelable(Shared.EXTRA_ROOT, root);
1746        args.putParcelable(Shared.EXTRA_DOC, doc);
1747        args.putString(Shared.EXTRA_QUERY, query);
1748        args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
1749
1750        final FragmentTransaction ft = fm.beginTransaction();
1751        AnimationView.setupAnimations(ft, anim, args);
1752
1753        final DirectoryFragment fragment = new DirectoryFragment();
1754        fragment.setArguments(args);
1755
1756        ft.replace(getFragmentId(), fragment);
1757        ft.commitAllowingStateLoss();
1758    }
1759
1760    private static String buildStateKey(RootInfo root, DocumentInfo doc) {
1761        final StringBuilder builder = new StringBuilder();
1762        builder.append(root != null ? root.authority : "null").append(';');
1763        builder.append(root != null ? root.rootId : "null").append(';');
1764        builder.append(doc != null ? doc.documentId : "null");
1765        return builder.toString();
1766    }
1767
1768    public static @Nullable DirectoryFragment get(FragmentManager fm) {
1769        // TODO: deal with multiple directories shown at once
1770        Fragment fragment = fm.findFragmentById(getFragmentId());
1771        return fragment instanceof DirectoryFragment
1772                ? (DirectoryFragment) fragment
1773                : null;
1774    }
1775
1776    private static int getFragmentId() {
1777        return R.id.container_directory;
1778    }
1779
1780    @Override
1781    public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
1782        Context context = getActivity();
1783        State state = getDisplayState();
1784
1785        Uri contentsUri;
1786        switch (mType) {
1787            case TYPE_NORMAL:
1788                contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
1789                        mRoot.authority, mRoot.rootId, mQuery)
1790                        : DocumentsContract.buildChildDocumentsUri(
1791                                mDocument.authority, mDocument.documentId);
1792                if (mTuner.managedModeEnabled()) {
1793                    contentsUri = DocumentsContract.setManageMode(contentsUri);
1794                }
1795                return new DirectoryLoader(
1796                        context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
1797                        mSearchMode);
1798            case TYPE_RECENT_OPEN:
1799                final RootsCache roots = DocumentsApplication.getRootsCache(context);
1800                return new RecentsLoader(context, roots, state);
1801
1802            default:
1803                throw new IllegalStateException("Unknown type " + mType);
1804        }
1805    }
1806
1807    @Override
1808    public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
1809        if (!isAdded()) return;
1810
1811        if (mSearchMode) {
1812            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
1813        }
1814
1815        State state = getDisplayState();
1816
1817        mAdapter.notifyDataSetChanged();
1818        mModel.update(result);
1819
1820        state.derivedSortOrder = result.sortOrder;
1821
1822        updateLayout(state.derivedMode);
1823
1824        if (mSelection != null) {
1825            mSelectionManager.setItemsSelected(mSelection.toList(), true);
1826            mSelection.clear();
1827        }
1828
1829        // Restore any previous instance state
1830        final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
1831        if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
1832            getView().restoreHierarchyState(container);
1833        } else if (mLastSortOrder != state.derivedSortOrder) {
1834            // The derived sort order takes the user sort order into account, but applies
1835            // directory-specific defaults when the user doesn't explicitly set the sort
1836            // order. Scroll to the top if the sort order actually changed.
1837            mRecView.smoothScrollToPosition(0);
1838        }
1839
1840        mLastSortOrder = state.derivedSortOrder;
1841
1842        mTuner.onModelLoaded(mModel, mType, mSearchMode);
1843
1844    }
1845
1846    @Override
1847    public void onLoaderReset(Loader<DirectoryResult> loader) {
1848        mModel.update(null);
1849    }
1850  }
1851