Revert "Revert "Bug: 29224737 Merge branch 'test-migrate' into master""

Bug: 29224737
This reverts commit 6d7ad6a86639e0cb1d855e705c5946e107ccfd6b.
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
new file mode 100644
index 0000000..db19881
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -0,0 +1,1711 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.State.MODE_GRID;
+import static com.android.documentsui.State.MODE_LIST;
+import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
+import static com.android.documentsui.model.DocumentInfo.getCursorInt;
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
+
+import android.annotation.IntDef;
+import android.annotation.StringRes;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.support.v13.view.DragStartHelper;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.RecyclerListener;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.text.BidiFormatter;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.DragEvent;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import com.android.documentsui.BaseActivity;
+import com.android.documentsui.DirectoryLoader;
+import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.DocumentsActivity;
+import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.Events.MotionInputEvent;
+import com.android.documentsui.ItemDragListener;
+import com.android.documentsui.MenuManager;
+import com.android.documentsui.Menus;
+import com.android.documentsui.MessageBar;
+import com.android.documentsui.Metrics;
+import com.android.documentsui.MimePredicate;
+import com.android.documentsui.R;
+import com.android.documentsui.RecentsLoader;
+import com.android.documentsui.RetainedState;
+import com.android.documentsui.RootsCache;
+import com.android.documentsui.Shared;
+import com.android.documentsui.Snackbars;
+import com.android.documentsui.State;
+import com.android.documentsui.State.ViewMode;
+import com.android.documentsui.ThumbnailCache;
+import com.android.documentsui.clipping.DocumentClipper;
+import com.android.documentsui.clipping.UrisSupplier;
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.RootInfo;
+import com.android.documentsui.services.FileOperation;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.services.FileOperations;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+/**
+ * Display the documents inside a single directory.
+ */
+public class DirectoryFragment extends Fragment
+        implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult>,
+        ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
+
+    @IntDef(flag = true, value = {
+            TYPE_NORMAL,
+            TYPE_RECENT_OPEN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultType {}
+    public static final int TYPE_NORMAL = 1;
+    public static final int TYPE_RECENT_OPEN = 2;
+
+    @IntDef(flag = true, value = {
+            REQUEST_COPY_DESTINATION
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RequestCode {}
+    public static final int REQUEST_COPY_DESTINATION = 1;
+
+    private static final String TAG = "DirectoryFragment";
+    private static final int LOADER_ID = 42;
+
+    private static final int CACHE_EVICT_LIMIT = 100;
+    private static final int REFRESH_SPINNER_DISMISS_DELAY = 500;
+
+    private Model mModel;
+    private MultiSelectManager mSelectionMgr;
+    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
+    private UserInputHandler<InputEvent> mInputHandler;
+    private SelectionModeListener mSelectionModeListener;
+    private FocusManager mFocusManager;
+
+    private IconHelper mIconHelper;
+
+    private SwipeRefreshLayout mRefreshLayout;
+    private View mEmptyView;
+    private RecyclerView mRecView;
+    private ListeningGestureDetector mGestureDetector;
+
+    private String mStateKey;
+
+    private int mLastSortOrder = SORT_ORDER_UNKNOWN;
+    private DocumentsAdapter mAdapter;
+    private FragmentTuner mTuner;
+    private DocumentClipper mClipper;
+    private GridLayoutManager mLayout;
+    private int mColumnCount = 1;  // This will get updated when layout changes.
+
+    private LayoutInflater mInflater;
+    private MessageBar mMessageBar;
+    private View mProgressBar;
+
+    // Directory fragment state is defined by: root, document, query, type, selection
+    private @ResultType int mType = TYPE_NORMAL;
+    private RootInfo mRoot;
+    private DocumentInfo mDocument;
+    private String mQuery = null;
+    // Note, we use !null to indicate that selection was restored (from rotation).
+    // So don't fiddle with this field unless you've got the bigger picture in mind.
+    private @Nullable Selection mRestoredSelection = null;
+    // Here we save the clip details of moveTo/copyTo actions when picker shows up.
+    // This will be written to saved instance.
+    private @Nullable FileOperation mPendingOperation;
+    private boolean mSearchMode = false;
+
+    private @Nullable BandController mBandController;
+    private @Nullable ActionMode mActionMode;
+
+    private DragScrollListener mOnDragListener;
+    private MenuManager mMenuManager;
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        mInflater = inflater;
+        final View view = inflater.inflate(R.layout.fragment_directory, container, false);
+
+        mMessageBar = MessageBar.create(getChildFragmentManager());
+        mProgressBar = view.findViewById(R.id.progressbar);
+
+        mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
+        mRefreshLayout.setOnRefreshListener(this);
+
+        mEmptyView = view.findViewById(android.R.id.empty);
+        mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
+        mRecView.setRecyclerListener(
+                new RecyclerListener() {
+                    @Override
+                    public void onViewRecycled(ViewHolder holder) {
+                        cancelThumbnailTask(holder.itemView);
+                    }
+                });
+
+        mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
+
+        mOnDragListener = DragScrollListener.create(
+                getActivity(), new DirectoryDragListener(this), mRecView);
+
+        // Make the recycler and the empty views responsive to drop events.
+        mRecView.setOnDragListener(mOnDragListener);
+        mEmptyView.setOnDragListener(mOnDragListener);
+
+        return view;
+    }
+
+    @Override
+    public void onDestroyView() {
+        mSelectionMgr.clearSelection();
+
+        // Cancel any outstanding thumbnail requests
+        final int count = mRecView.getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View view = mRecView.getChildAt(i);
+            cancelThumbnailTask(view);
+        }
+
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        final Context context = getActivity();
+        final State state = getDisplayState();
+
+        // Read arguments when object created for the first time.
+        // Restore state if fragment recreated.
+        Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState;
+        mRoot = args.getParcelable(Shared.EXTRA_ROOT);
+        mDocument = args.getParcelable(Shared.EXTRA_DOC);
+        mStateKey = buildStateKey(mRoot, mDocument);
+        mQuery = args.getString(Shared.EXTRA_QUERY);
+        mType = args.getInt(Shared.EXTRA_TYPE);
+        mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
+        mPendingOperation = args.getParcelable(FileOperationService.EXTRA_OPERATION);
+
+        // Restore any selection we may have squirreled away in retained state.
+        @Nullable RetainedState retained = getBaseActivity().getRetainedState();
+        if (retained != null && retained.hasSelection()) {
+            // We claim the selection for ourselves and null it out once used
+            // so we don't have a rando selection hanging around in RetainedState.
+            mRestoredSelection = retained.selection;
+            retained.selection = null;
+        }
+
+        mIconHelper = new IconHelper(context, MODE_GRID);
+
+        mAdapter = new SectionBreakDocumentsAdapterWrapper(
+                this, new ModelBackedDocumentsAdapter(this, mIconHelper));
+
+        mRecView.setAdapter(mAdapter);
+
+        mLayout = new GridLayoutManager(getContext(), mColumnCount);
+        SpanSizeLookup lookup = mAdapter.createSpanSizeLookup();
+        if (lookup != null) {
+            mLayout.setSpanSizeLookup(lookup);
+        }
+        mRecView.setLayoutManager(mLayout);
+
+        // TODO: instead of inserting the view into the constructor, extract listener-creation code
+        // and set the listener on the view after the fact.  Then the view doesn't need to be passed
+        // into the selection manager.
+        mSelectionMgr = new MultiSelectManager(
+                mAdapter,
+                state.allowMultiple
+                    ? MultiSelectManager.MODE_MULTIPLE
+                    : MultiSelectManager.MODE_SINGLE);
+
+        // Make sure this is done after the RecyclerView is set up.
+        mFocusManager = new FocusManager(context, mRecView, mModel);
+
+        mInputHandler = new UserInputHandler<>(
+                mSelectionMgr,
+                mFocusManager,
+                new Function<MotionEvent, InputEvent>() {
+                    @Override
+                    public InputEvent apply(MotionEvent t) {
+                        return MotionInputEvent.obtain(t, mRecView);
+                    }
+                },
+                this::getTarget,
+                this::canSelect,
+                this::onRightClick,
+                this::onActivate,
+                (DocumentDetails ignored) -> {
+                    return onDeleteSelectedDocuments();
+                });
+
+        mGestureDetector =
+                new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler);
+
+        mRecView.addOnItemTouchListener(mGestureDetector);
+        mEmptyView.setOnTouchListener(mGestureDetector);
+
+        if (state.allowMultiple) {
+            mBandController = new BandController(mRecView, mAdapter, mSelectionMgr);
+        }
+
+        mSelectionModeListener = new SelectionModeListener();
+        mSelectionMgr.addCallback(mSelectionModeListener);
+
+        mModel = new Model();
+        mModel.addUpdateListener(mAdapter);
+        mModel.addUpdateListener(mModelUpdateListener);
+
+        final BaseActivity activity = getBaseActivity();
+        mTuner = activity.createFragmentTuner();
+        mMenuManager = activity.getMenuManager();
+        mClipper = DocumentsApplication.getDocumentClipper(getContext());
+
+        final ActivityManager am = (ActivityManager) context.getSystemService(
+                Context.ACTIVITY_SERVICE);
+        boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
+        mIconHelper.setThumbnailsEnabled(!svelte);
+
+        // Kick off loader at least once
+        getLoaderManager().restartLoader(LOADER_ID, null, this);
+    }
+
+    public void retainState(RetainedState state) {
+        state.selection = mSelectionMgr.getSelection(new Selection());
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putInt(Shared.EXTRA_TYPE, mType);
+        outState.putParcelable(Shared.EXTRA_ROOT, mRoot);
+        outState.putParcelable(Shared.EXTRA_DOC, mDocument);
+        outState.putString(Shared.EXTRA_QUERY, mQuery);
+        outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
+        outState.putParcelable(FileOperationService.EXTRA_OPERATION, mPendingOperation);
+    }
+
+    @Override
+    public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_COPY_DESTINATION:
+                handleCopyResult(resultCode, data);
+                break;
+            default:
+                throw new UnsupportedOperationException("Unknown request code: " + requestCode);
+        }
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu,
+            View v,
+            ContextMenu.ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, v, menuInfo);
+        MenuInflater inflater = getActivity().getMenuInflater();
+        inflater.inflate(R.menu.context_menu, menu);
+
+        menu.add(Menu.NONE, R.id.menu_create_dir, Menu.NONE, R.string.menu_create_dir);
+        menu.add(Menu.NONE, R.id.menu_delete, Menu.NONE, R.string.menu_delete);
+        menu.add(Menu.NONE, R.id.menu_rename, Menu.NONE, R.string.menu_rename);
+
+        if (v == mRecView || v == mEmptyView) {
+            mMenuManager.updateContextMenu(menu, null, getBaseActivity().getDirectoryDetails());
+        } else {
+            mMenuManager.updateContextMenu(menu, mSelectionModeListener,
+                    getBaseActivity().getDirectoryDetails());
+        }
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        return handleMenuItemClick(item);
+    }
+
+    private void handleCopyResult(int resultCode, Intent data) {
+
+        FileOperation operation = mPendingOperation;
+        mPendingOperation = null;
+
+        if (resultCode == Activity.RESULT_CANCELED || data == null) {
+            // User pressed the back button or otherwise cancelled the destination pick. Don't
+            // proceed with the copy.
+            operation.dispose();
+            return;
+        }
+
+        operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
+
+        BaseActivity activity = getBaseActivity();
+        FileOperations.start(activity, operation, activity.fileOpCallback);
+    }
+
+    protected boolean onRightClick(InputEvent e) {
+        if (e.getItemPosition() != RecyclerView.NO_POSITION) {
+            final DocumentHolder doc = getTarget(e);
+            if (!mSelectionMgr.getSelection().contains(doc.modelId)) {
+                mSelectionMgr.replaceSelection(Collections.singleton(doc.modelId));
+            }
+
+            // We are registering for context menu here so long-press doesn't trigger this
+            // floating context menu, and then quickly unregister right afterwards
+            registerForContextMenu(doc.itemView);
+            mRecView.showContextMenuForChild(doc.itemView,
+                    e.getX() - doc.itemView.getLeft(), e.getY() - doc.itemView.getTop());
+            unregisterForContextMenu(doc.itemView);
+            return true;
+        }
+
+        // If there was no corresponding item pos, that means user right-clicked on the blank
+        // pane
+        // We would want to show different options then, and not select any item
+        // The blank pane could be the recyclerView or the emptyView, so we need to register
+        // according to whichever one is visible
+        if (mEmptyView.getVisibility() == View.VISIBLE) {
+            registerForContextMenu(mEmptyView);
+            mEmptyView.showContextMenu(e.getX(), e.getY());
+            unregisterForContextMenu(mEmptyView);
+            return true;
+        }
+
+        registerForContextMenu(mRecView);
+        mRecView.showContextMenu(e.getX(), e.getY());
+        unregisterForContextMenu(mRecView);
+        return true;
+    }
+
+    private boolean handleViewItem(String id) {
+        final Cursor cursor = mModel.getItem(id);
+
+        if (cursor == null) {
+            Log.w(TAG, "Can't view item. Can't obtain cursor for modeId" + id);
+            return false;
+        }
+
+        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
+            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
+            getBaseActivity().onDocumentPicked(doc, mModel);
+            mSelectionMgr.clearSelection();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // Remember last scroll location
+        final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
+        getView().saveHierarchyState(container);
+        final State state = getDisplayState();
+        state.dirState.put(mStateKey, container);
+    }
+
+    public void onDisplayStateChanged() {
+        updateDisplayState();
+    }
+
+    public void onSortOrderChanged() {
+        // Sort order is implemented as a sorting wrapper around directory
+        // results. So when sort order changes, we force a reload of the directory.
+        getLoaderManager().restartLoader(LOADER_ID, null, this);
+    }
+
+    public void onViewModeChanged() {
+        // Mode change is just visual change; no need to kick loader.
+        updateDisplayState();
+    }
+
+    private void updateDisplayState() {
+        State state = getDisplayState();
+        updateLayout(state.derivedMode);
+        mRecView.setAdapter(mAdapter);
+    }
+
+    /**
+     * Updates the layout after the view mode switches.
+     * @param mode The new view mode.
+     */
+    private void updateLayout(@ViewMode int mode) {
+        mColumnCount = calculateColumnCount(mode);
+        if (mLayout != null) {
+            mLayout.setSpanCount(mColumnCount);
+        }
+
+        int pad = getDirectoryPadding(mode);
+        mRecView.setPadding(pad, pad, pad, pad);
+        mRecView.requestLayout();
+        if (mBandController != null) {
+            mBandController.handleLayoutChanged();
+        }
+        mIconHelper.setViewMode(mode);
+    }
+
+    private int calculateColumnCount(@ViewMode int mode) {
+        if (mode == MODE_LIST) {
+            // List mode is a "grid" with 1 column.
+            return 1;
+        }
+
+        int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
+        int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
+        int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
+
+        // RecyclerView sometimes gets a width of 0 (see b/27150284).  Clamp so that we always lay
+        // out the grid with at least 2 columns.
+        int columnCount = Math.max(2,
+                (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
+
+        return columnCount;
+    }
+
+    private int getDirectoryPadding(@ViewMode int mode) {
+        switch (mode) {
+            case MODE_GRID:
+                return getResources().getDimensionPixelSize(R.dimen.grid_container_padding);
+            case MODE_LIST:
+                return getResources().getDimensionPixelSize(R.dimen.list_container_padding);
+            default:
+                throw new IllegalArgumentException("Unsupported layout mode: " + mode);
+        }
+    }
+
+    @Override
+    public int getColumnCount() {
+        return mColumnCount;
+    }
+
+    // Support method to replace getOwner().foo() with something
+    // slightly less clumsy like: getOwner().foo().
+    private BaseActivity getBaseActivity() {
+        return (BaseActivity) getActivity();
+    }
+
+    /**
+     * Manages the integration between our ActionMode and MultiSelectManager, initiating
+     * ActionMode when there is a selection, canceling it when there is no selection,
+     * and clearing selection when action mode is explicitly exited by the user.
+     */
+    private final class SelectionModeListener implements MultiSelectManager.Callback,
+            ActionMode.Callback, MenuManager.SelectionDetails {
+
+        private Selection mSelected = new Selection();
+
+        // Partial files are files that haven't been fully downloaded.
+        private int mPartialCount = 0;
+        private int mDirectoryCount = 0;
+        private int mNoDeleteCount = 0;
+        private int mNoRenameCount = 0;
+
+        private Menu mMenu;
+
+        @Override
+        public boolean onBeforeItemStateChange(String modelId, boolean selected) {
+            if (selected) {
+                final Cursor cursor = mModel.getItem(modelId);
+                if (cursor == null) {
+                    Log.w(TAG, "Can't obtain cursor for modelId: " + modelId);
+                    return false;
+                }
+
+                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+                final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+                if (!mTuner.canSelectType(docMimeType, docFlags)) {
+                    return false;
+                }
+                return mTuner.canSelectType(docMimeType, docFlags);
+            }
+            return true;
+        }
+
+        @Override
+        public void onItemStateChanged(String modelId, boolean selected) {
+            final Cursor cursor = mModel.getItem(modelId);
+            if (cursor == null) {
+                Log.w(TAG, "Model returned null cursor for document: " + modelId
+                        + ". Ignoring state changed event.");
+                return;
+            }
+
+            // TODO: Should this be happening in onSelectionChanged? Technically this callback is
+            // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
+            // selection changes here)
+            final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            if (MimePredicate.isDirectoryType(mimeType)) {
+                mDirectoryCount += selected ? 1 : -1;
+            }
+
+            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+            if ((docFlags & Document.FLAG_PARTIAL) != 0) {
+                mPartialCount += selected ? 1 : -1;
+            }
+            if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
+                mNoDeleteCount += selected ? 1 : -1;
+            }
+            if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) {
+                mNoRenameCount += selected ? 1 : -1;
+            }
+        }
+
+        @Override
+        public void onSelectionChanged() {
+            mSelectionMgr.getSelection(mSelected);
+            if (mSelected.size() > 0) {
+                if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
+                if (mActionMode == null) {
+                    if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
+                    mActionMode = getActivity().startActionMode(this);
+                }
+                updateActionMenu();
+            } else {
+                if (DEBUG) Log.d(TAG, "Finishing action mode.");
+                if (mActionMode != null) {
+                    mActionMode.finish();
+                }
+            }
+
+            if (mActionMode != null) {
+                assert(!mSelected.isEmpty());
+                final String title = Shared.getQuantityString(getActivity(),
+                        R.plurals.elements_selected, mSelected.size());
+                mActionMode.setTitle(title);
+                mRecView.announceForAccessibility(title);
+            }
+        }
+
+        // Called when the user exits the action mode
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
+            mActionMode = null;
+            // clear selection
+            mSelectionMgr.clearSelection();
+            mSelected.clear();
+
+            mDirectoryCount = 0;
+            mPartialCount = 0;
+            mNoDeleteCount = 0;
+            mNoRenameCount = 0;
+
+            // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
+            final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
+            toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+
+            // This toolbar is not present in the fixed_layout
+            final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar);
+            if (rootsToolbar != null) {
+                rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            }
+        }
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            if (mRestoredSelection != null) {
+                // This is a careful little song and dance to avoid haptic feedback
+                // when selection has been restored after rotation. We're
+                // also responsible for cleaning up restored selection so the
+                // object dones't unnecessarily hang around.
+                mRestoredSelection = null;
+            } else {
+                mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            }
+
+            int size = mSelectionMgr.getSelection().size();
+            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
+            mode.setTitle(TextUtils.formatSelectedCount(size));
+
+            if (size > 0) {
+                // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
+                // these controls when using linear navigation.
+                final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
+                toolbar.setImportantForAccessibility(
+                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+
+                // This toolbar is not present in the fixed_layout
+                final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
+                        R.id.roots_toolbar);
+                if (rootsToolbar != null) {
+                    rootsToolbar.setImportantForAccessibility(
+                            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+                }
+                return true;
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            mMenu = menu;
+            updateActionMenu();
+            return true;
+        }
+
+        @Override
+        public boolean containsDirectories() {
+            return mDirectoryCount > 0;
+        }
+
+        @Override
+        public boolean containsPartialFiles() {
+            return mPartialCount > 0;
+        }
+
+        @Override
+        public boolean canDelete() {
+            return mNoDeleteCount == 0;
+        }
+
+        @Override
+        public boolean canRename() {
+            return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1;
+        }
+
+        private void updateActionMenu() {
+            assert(mMenu != null);
+            mMenuManager.updateActionMenu(mMenu, this);
+            Menus.disableHiddenItems(mMenu);
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            return handleMenuItemClick(item);
+        }
+    }
+
+    private boolean handleMenuItemClick(MenuItem item) {
+        Selection selection = mSelectionMgr.getSelection(new Selection());
+
+        switch (item.getItemId()) {
+            case R.id.menu_open:
+                openDocuments(selection);
+                mActionMode.finish();
+                return true;
+
+            case R.id.menu_share:
+                shareDocuments(selection);
+                // TODO: Only finish selection if share action is completed.
+                mActionMode.finish();
+                return true;
+
+            case R.id.menu_delete:
+                // deleteDocuments will end action mode if the documents are deleted.
+                // It won't end action mode if user cancels the delete.
+                deleteDocuments(selection);
+                return true;
+
+            case R.id.menu_copy_to:
+                transferDocuments(selection, FileOperationService.OPERATION_COPY);
+                // TODO: Only finish selection mode if copy-to is not canceled.
+                // Need to plum down into handling the way we do with deleteDocuments.
+                mActionMode.finish();
+                return true;
+
+            case R.id.menu_move_to:
+                // Exit selection mode first, so we avoid deselecting deleted documents.
+                mActionMode.finish();
+                transferDocuments(selection, FileOperationService.OPERATION_MOVE);
+                return true;
+
+            case R.id.menu_cut_to_clipboard:
+                cutSelectedToClipboard();
+                return true;
+
+            case R.id.menu_copy_to_clipboard:
+                copySelectedToClipboard();
+                return true;
+
+            case R.id.menu_paste_from_clipboard:
+                pasteFromClipboard();
+                return true;
+
+            case R.id.menu_select_all:
+                selectAllFiles();
+                return true;
+
+            case R.id.menu_rename:
+                // Exit selection mode first, so we avoid deselecting deleted
+                // (renamed) documents.
+                mActionMode.finish();
+                renameDocuments(selection);
+                return true;
+
+            default:
+                // See if BaseActivity can handle this particular MenuItem
+                if (!getBaseActivity().onOptionsItemSelected(item)) {
+                    if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
+                    return false;
+                }
+                return true;
+        }
+    }
+
+    public final boolean onBackPressed() {
+        if (mSelectionMgr.hasSelection()) {
+            if (DEBUG) Log.d(TAG, "Clearing selection on selection manager.");
+            mSelectionMgr.clearSelection();
+            return true;
+        }
+        return false;
+    }
+
+    private void cancelThumbnailTask(View view) {
+        final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
+        if (iconThumb != null) {
+            mIconHelper.stopLoading(iconThumb);
+        }
+    }
+
+    private void openDocuments(final Selection selected) {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_OPEN);
+
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                // TODO: Implement support in Files activity for opening multiple docs.
+                BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
+            }
+        }.execute(selected);
+    }
+
+    private void shareDocuments(final Selection selected) {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
+
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                Intent intent;
+
+                // Filter out directories and virtual files - those can't be shared.
+                List<DocumentInfo> docsForSend = new ArrayList<>();
+                for (DocumentInfo doc: docs) {
+                    if (!doc.isDirectory() && !doc.isVirtualDocument()) {
+                        docsForSend.add(doc);
+                    }
+                }
+
+                if (docsForSend.size() == 1) {
+                    final DocumentInfo doc = docsForSend.get(0);
+
+                    intent = new Intent(Intent.ACTION_SEND);
+                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                    intent.addCategory(Intent.CATEGORY_DEFAULT);
+                    intent.setType(doc.mimeType);
+                    intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
+
+                } else if (docsForSend.size() > 1) {
+                    intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                    intent.addCategory(Intent.CATEGORY_DEFAULT);
+
+                    final ArrayList<String> mimeTypes = new ArrayList<>();
+                    final ArrayList<Uri> uris = new ArrayList<>();
+                    for (DocumentInfo doc : docsForSend) {
+                        mimeTypes.add(doc.mimeType);
+                        uris.add(doc.derivedUri);
+                    }
+
+                    intent.setType(findCommonMimeType(mimeTypes));
+                    intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+
+                } else {
+                    return;
+                }
+
+                intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
+                startActivity(intent);
+            }
+        }.execute(selected);
+    }
+
+    private String generateDeleteMessage(final List<DocumentInfo> docs) {
+        String message;
+        int dirsCount = 0;
+
+        for (DocumentInfo doc : docs) {
+            if (doc.isDirectory()) {
+                ++dirsCount;
+            }
+        }
+
+        if (docs.size() == 1) {
+            // Deleteing 1 file xor 1 folder in cwd
+
+            // Address b/28772371, where including user strings in message can result in
+            // broken bidirectional support.
+            String displayName = BidiFormatter.getInstance().unicodeWrap(docs.get(0).displayName);
+            message = dirsCount == 0
+                    ? getActivity().getString(R.string.delete_filename_confirmation_message,
+                            displayName)
+                    : getActivity().getString(R.string.delete_foldername_confirmation_message,
+                            displayName);
+        } else if (dirsCount == 0) {
+            // Deleting only files in cwd
+            message = Shared.getQuantityString(getActivity(),
+                    R.plurals.delete_files_confirmation_message, docs.size());
+        } else if (dirsCount == docs.size()) {
+            // Deleting only folders in cwd
+            message = Shared.getQuantityString(getActivity(),
+                    R.plurals.delete_folders_confirmation_message, docs.size());
+        } else {
+            // Deleting mixed items (files and folders) in cwd
+            message = Shared.getQuantityString(getActivity(),
+                    R.plurals.delete_items_confirmation_message, docs.size());
+        }
+        return message;
+    }
+
+    private boolean onDeleteSelectedDocuments() {
+        if (mSelectionMgr.hasSelection()) {
+            deleteDocuments(mSelectionMgr.getSelection(new Selection()));
+        }
+        return false;
+    }
+
+    private boolean onActivate(DocumentDetails doc) {
+        // Toggle selection if we're in selection mode, othewise, view item.
+        if (mSelectionMgr.hasSelection()) {
+            mSelectionMgr.toggleSelection(doc.getModelId());
+        } else {
+            handleViewItem(doc.getModelId());
+        }
+        return true;
+    }
+
+//    private boolean onSelect(DocumentDetails doc) {
+//        mSelectionMgr.toggleSelection(doc.getModelId());
+//        mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
+//        return true;
+//    }
+
+    private void deleteDocuments(final Selection selected) {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_DELETE);
+
+        assert(!selected.isEmpty());
+
+        final DocumentInfo srcParent = getDisplayState().stack.peek();
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(final List<DocumentInfo> docs) {
+
+                TextView message =
+                        (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
+                message.setText(generateDeleteMessage(docs));
+
+                // For now, we implement this dialog NOT
+                // as a fragment (which can survive rotation and have its own state),
+                // but as a simple runtime dialog. So rotating a device with an
+                // active delete dialog...results in that dialog disappearing.
+                // We can do better, but don't have cycles for it now.
+                new AlertDialog.Builder(getActivity())
+                    .setView(message)
+                    .setPositiveButton(
+                         android.R.string.ok,
+                         new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int id) {
+                                // Finish selection mode first which clears selection so we
+                                // don't end up trying to deselect deleted documents.
+                                // This is done here, rather in the onActionItemClicked
+                                // so we can avoid de-selecting items in the case where
+                                // the user cancels the delete.
+                                if (mActionMode != null) {
+                                    mActionMode.finish();
+                                } else {
+                                    Log.w(TAG, "Action mode is null before deleting documents.");
+                                }
+
+                                UrisSupplier srcs;
+                                try {
+                                    srcs = UrisSupplier.create(
+                                            selected,
+                                            mModel::getItemUri,
+                                            getContext());
+                                } catch(IOException e) {
+                                    throw new RuntimeException("Failed to create uri supplier.", e);
+                                }
+
+                                FileOperation operation = new FileOperation.Builder()
+                                        .withOpType(FileOperationService.OPERATION_DELETE)
+                                        .withDestination(getDisplayState().stack)
+                                        .withSrcs(srcs)
+                                        .withSrcParent(srcParent.derivedUri)
+                                        .build();
+
+                                BaseActivity activity = getBaseActivity();
+                                FileOperations.start(activity, operation, activity.fileOpCallback);
+                            }
+                        })
+                    .setNegativeButton(android.R.string.cancel, null)
+                    .show();
+            }
+        }.execute(selected);
+    }
+
+    private void transferDocuments(final Selection selected, final @OpType int mode) {
+        if(mode == FileOperationService.OPERATION_COPY) {
+            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_TO);
+        } else if (mode == FileOperationService.OPERATION_MOVE) {
+            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_MOVE_TO);
+        }
+
+        // Pop up a dialog to pick a destination.  This is inadequate but works for now.
+        // TODO: Implement a picker that is to spec.
+        final Intent intent = new Intent(
+                Shared.ACTION_PICK_COPY_DESTINATION,
+                Uri.EMPTY,
+                getActivity(),
+                DocumentsActivity.class);
+
+        UrisSupplier srcs;
+        try {
+            srcs = UrisSupplier.create(selected, mModel::getItemUri, getContext());
+        } catch(IOException e) {
+            throw new RuntimeException("Failed to create uri supplier.", e);
+        }
+
+        Uri srcParent = getDisplayState().stack.peek().derivedUri;
+        mPendingOperation = new FileOperation.Builder()
+                .withOpType(mode)
+                .withSrcParent(srcParent)
+                .withSrcs(srcs)
+                .build();
+
+        // Relay any config overrides bits present in the original intent.
+        Intent original = getActivity().getIntent();
+        if (original != null && original.hasExtra(Shared.EXTRA_PRODUCTIVITY_MODE)) {
+            intent.putExtra(
+                    Shared.EXTRA_PRODUCTIVITY_MODE,
+                    original.getBooleanExtra(Shared.EXTRA_PRODUCTIVITY_MODE, false));
+        }
+
+        // Set an appropriate title on the drawer when it is shown in the picker.
+        // Coupled with the fact that we auto-open the drawer for copy/move operations
+        // it should basically be the thing people see first.
+        int drawerTitleId = mode == FileOperationService.OPERATION_MOVE
+                ? R.string.menu_move : R.string.menu_copy;
+        intent.putExtra(DocumentsContract.EXTRA_PROMPT, getResources().getString(drawerTitleId));
+
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                // Determine if there is a directory in the set of documents
+                // to be copied? Why? Directory creation isn't supported by some roots
+                // (like Downloads). This informs DocumentsActivity (the "picker")
+                // to restrict available roots to just those with support.
+                intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
+                intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode);
+
+                // This just identifies the type of request...we'll check it
+                // when we reveive a response.
+                startActivityForResult(intent, REQUEST_COPY_DESTINATION);
+            }
+
+        }.execute(selected);
+    }
+
+    private static boolean hasDirectory(List<DocumentInfo> docs) {
+        for (DocumentInfo info : docs) {
+            if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void renameDocuments(Selection selected) {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_RENAME);
+
+        // Batch renaming not supported
+        // Rename option is only available in menu when 1 document selected
+        assert(selected.size() == 1);
+
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
+            }
+        }.execute(selected);
+    }
+
+    @Override
+    public void initDocumentHolder(DocumentHolder holder) {
+        holder.addKeyEventListener(mInputHandler);
+        holder.itemView.setOnFocusChangeListener(mFocusManager);
+    }
+
+    @Override
+    public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
+        setupDragAndDropOnDocumentView(holder.itemView, cursor);
+    }
+
+    @Override
+    public State getDisplayState() {
+        return getBaseActivity().getDisplayState();
+    }
+
+    @Override
+    public Model getModel() {
+        return mModel;
+    }
+
+    @Override
+    public boolean isDocumentEnabled(String docMimeType, int docFlags) {
+        return mTuner.isDocumentEnabled(docMimeType, docFlags);
+    }
+
+    private void showEmptyDirectory() {
+        showEmptyView(R.string.empty, R.drawable.cabinet);
+    }
+
+    private void showNoResults(RootInfo root) {
+        CharSequence msg = getContext().getResources().getText(R.string.no_results);
+        showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
+    }
+
+    private void showQueryError() {
+        showEmptyView(R.string.query_error, R.drawable.hourglass);
+    }
+
+    private void showEmptyView(@StringRes int id, int drawable) {
+        showEmptyView(getContext().getResources().getText(id), drawable);
+    }
+
+    private void showEmptyView(CharSequence msg, int drawable) {
+        View content = mEmptyView.findViewById(R.id.content);
+        TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
+        ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
+        msgView.setText(msg);
+        imageView.setImageResource(drawable);
+
+        mEmptyView.setVisibility(View.VISIBLE);
+        mEmptyView.requestFocus();
+        mRecView.setVisibility(View.GONE);
+    }
+
+    private void showDirectory() {
+        mEmptyView.setVisibility(View.GONE);
+        mRecView.setVisibility(View.VISIBLE);
+        mRecView.requestFocus();
+    }
+
+    private String findCommonMimeType(List<String> mimeTypes) {
+        String[] commonType = mimeTypes.get(0).split("/");
+        if (commonType.length != 2) {
+            return "*/*";
+        }
+
+        for (int i = 1; i < mimeTypes.size(); i++) {
+            String[] type = mimeTypes.get(i).split("/");
+            if (type.length != 2) continue;
+
+            if (!commonType[1].equals(type[1])) {
+                commonType[1] = "*";
+            }
+
+            if (!commonType[0].equals(type[0])) {
+                commonType[0] = "*";
+                commonType[1] = "*";
+                break;
+            }
+        }
+
+        return commonType[0] + "/" + commonType[1];
+    }
+
+    public void copySelectedToClipboard() {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
+
+        Selection selection = mSelectionMgr.getSelection(new Selection());
+        if (selection.isEmpty()) {
+            return;
+        }
+        mSelectionMgr.clearSelection();
+
+        mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
+
+        Snackbars.showDocumentsClipped(getActivity(), selection.size());
+    }
+
+    public void cutSelectedToClipboard() {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD);
+
+        Selection selection = mSelectionMgr.getSelection(new Selection());
+        if (selection.isEmpty()) {
+            return;
+        }
+        mSelectionMgr.clearSelection();
+
+        mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
+
+        Snackbars.showDocumentsClipped(getActivity(), selection.size());
+    }
+
+    public void pasteFromClipboard() {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
+
+        BaseActivity activity = (BaseActivity) getActivity();
+        DocumentInfo destination = activity.getCurrentDirectory();
+        mClipper.copyFromClipboard(
+                destination, activity.getDisplayState().stack, activity.fileOpCallback);
+        getActivity().invalidateOptionsMenu();
+    }
+
+    public void selectAllFiles() {
+        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SELECT_ALL);
+
+        // Exclude disabled files
+        List<String> enabled = new ArrayList<String>();
+        for (String id : mAdapter.getModelIds()) {
+            Cursor cursor = getModel().getItem(id);
+            if (cursor == null) {
+                Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
+                continue;
+            }
+            String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+            if (isDocumentEnabled(docMimeType, docFlags)) {
+                enabled.add(id);
+            }
+        }
+
+        // Only select things currently visible in the adapter.
+        boolean changed = mSelectionMgr.setItemsSelected(enabled, true);
+        if (changed) {
+            updateDisplayState();
+        }
+    }
+
+    /**
+     * Attempts to restore focus on the directory listing.
+     */
+    public void requestFocus() {
+        mFocusManager.restoreLastFocus();
+    }
+
+    private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
+        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
+            // Make a directory item a drop target. Drop on non-directories and empty space
+            // is handled at the list/grid view level.
+            view.setOnDragListener(mOnDragListener);
+        }
+
+        if (mTuner.dragAndDropEnabled()) {
+            // Make all items draggable.
+            view.setOnLongClickListener(onLongClickListener);
+        }
+    }
+
+    void dragStarted() {
+        // When files are selected for dragging, ActionMode is started. This obscures the breadcrumb
+        // with an ActionBar. In order to make drag and drop to the breadcrumb possible, we first
+        // end ActionMode so the breadcrumb is visible to the user.
+        if (mActionMode != null) {
+            mActionMode.finish();
+        }
+    }
+
+    void dragStopped(boolean result) {
+        if (result) {
+            mSelectionMgr.clearSelection();
+        }
+    }
+
+    @Override
+    public void runOnUiThread(Runnable runnable) {
+        getActivity().runOnUiThread(runnable);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * In DirectoryFragment, we spring loads the hovered folder.
+     */
+    @Override
+    public void onViewHovered(View view) {
+        BaseActivity activity = (BaseActivity) getActivity();
+        if (getModelId(view) != null) {
+           activity.springOpenDirectory(getDestination(view));
+        }
+
+        activity.setRootsDrawerOpen(false);
+    }
+
+    boolean handleDropEvent(View v, DragEvent event) {
+        BaseActivity activity = (BaseActivity) getActivity();
+        activity.setRootsDrawerOpen(false);
+
+        ClipData clipData = event.getClipData();
+        assert (clipData != null);
+
+        assert(DocumentClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
+
+        // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
+        // multi-window drag, because localState isn't carried over from one process to
+        // another.
+        Object src = event.getLocalState();
+        DocumentInfo dst = getDestination(v);
+        if (Objects.equals(src, dst)) {
+            if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
+            return false;
+        }
+
+        // Recognize multi-window drag and drop based on the fact that localState is not
+        // carried between processes. It will stop working when the localsState behavior
+        // is changed. The info about window should be passed in the localState then.
+        // The localState could also be null for copying from Recents in single window
+        // mode, but Recents doesn't offer this functionality (no directories).
+        Metrics.logUserAction(getContext(),
+                src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
+                        : Metrics.USER_ACTION_DRAG_N_DROP);
+
+        mClipper.copyFromClipData(dst, getDisplayState().stack, clipData, activity.fileOpCallback);
+        return true;
+    }
+
+    private DocumentInfo getDestination(View v) {
+        String id = getModelId(v);
+        if (id != null) {
+            Cursor dstCursor = mModel.getItem(id);
+            if (dstCursor == null) {
+                Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
+                return null;
+            }
+            return DocumentInfo.fromDirectoryCursor(dstCursor);
+        }
+
+        if (v == mRecView || v == mEmptyView) {
+            return getDisplayState().stack.peek();
+        }
+
+        return null;
+    }
+
+    @Override
+    public void setDropTargetHighlight(View v, boolean highlight) {
+        // Note: use exact comparison - this code is searching for views which are children of
+        // the RecyclerView instance in the UI.
+        if (v.getParent() == mRecView) {
+            RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
+            if (vh instanceof DocumentHolder) {
+                ((DocumentHolder) vh).setHighlighted(highlight);
+            }
+        }
+    }
+
+    private @Nullable DocumentHolder getTarget(InputEvent e) {
+        View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
+        if (childView != null) {
+            return (DocumentHolder) mRecView.getChildViewHolder(childView);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Gets the model ID for a given RecyclerView item.
+     * @param view A View that is a document item view, or a child of a document item view.
+     * @return The Model ID for the given document, or null if the given view is not associated with
+     *     a document item view.
+     */
+    protected @Nullable String getModelId(View view) {
+        View itemView = mRecView.findContainingItemView(view);
+        if (itemView != null) {
+            RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
+            if (vh instanceof DocumentHolder) {
+                return ((DocumentHolder) vh).modelId;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Abstract task providing support for loading documents *off*
+     * the main thread. And if it isn't obvious, creating a list
+     * of documents (especially large lists) can be pretty expensive.
+     */
+    private abstract class GetDocumentsTask
+            extends AsyncTask<Selection, Void, List<DocumentInfo>> {
+        @Override
+        protected final List<DocumentInfo> doInBackground(Selection... selected) {
+            return mModel.getDocuments(selected[0]);
+        }
+
+        @Override
+        protected final void onPostExecute(List<DocumentInfo> docs) {
+            onDocumentsReady(docs);
+        }
+
+        abstract void onDocumentsReady(List<DocumentInfo> docs);
+    }
+
+    @Override
+    public boolean isSelected(String modelId) {
+        return mSelectionMgr.getSelection().contains(modelId);
+    }
+
+    private final class ModelUpdateListener implements Model.UpdateListener {
+        @Override
+        public void onModelUpdate(Model model) {
+            if (model.info != null || model.error != null) {
+                mMessageBar.setInfo(model.info);
+                mMessageBar.setError(model.error);
+                mMessageBar.show();
+            }
+
+            mProgressBar.setVisibility(model.isLoading() ? View.VISIBLE : View.GONE);
+
+            if (model.isEmpty()) {
+                if (mSearchMode) {
+                    showNoResults(getDisplayState().stack.root);
+                } else {
+                    showEmptyDirectory();
+                }
+            } else {
+                showDirectory();
+                mAdapter.notifyDataSetChanged();
+            }
+
+            if (!model.isLoading()) {
+                getBaseActivity().notifyDirectoryLoaded(
+                    model.doc != null ? model.doc.derivedUri : null);
+            }
+        }
+
+        @Override
+        public void onModelUpdateFailed(Exception e) {
+            showQueryError();
+        }
+    }
+
+    private Drawable getDragIcon(Selection selection) {
+        if (selection.size() == 1) {
+            DocumentInfo doc = getSingleSelectedDocument(selection);
+            return mIconHelper.getDocumentIcon(getContext(), doc);
+        }
+        return getContext().getDrawable(com.android.internal.R.drawable.ic_doc_generic);
+    }
+
+    private String getDragTitle(Selection selection) {
+        assert (!selection.isEmpty());
+        if (selection.size() == 1) {
+            DocumentInfo doc = getSingleSelectedDocument(selection);
+            return doc.displayName;
+        }
+
+        return Shared.getQuantityString(getContext(), R.plurals.elements_dragged, selection.size());
+    }
+
+    private DocumentInfo getSingleSelectedDocument(Selection selection) {
+        assert (selection.size() == 1);
+        final List<DocumentInfo> docs = mModel.getDocuments(mSelectionMgr.getSelection());
+        assert (docs.size() == 1);
+        return docs.get(0);
+    }
+
+    private DragStartHelper.OnDragStartListener mOnDragStartListener =
+            new DragStartHelper.OnDragStartListener() {
+                @Override
+                public boolean onDragStart(View v, DragStartHelper helper) {
+                    Selection selection = mSelectionMgr.getSelection();
+
+                    if (v == null) {
+                        Log.d(TAG, "Ignoring drag event, null view");
+                        return false;
+                    }
+                    if (!isSelected(getModelId(v))) {
+                        Log.d(TAG, "Ignoring drag event, unselected view.");
+                        return false;
+                    }
+
+                    // NOTE: Preparation of the ClipData object can require a lot of time
+                    // and ideally should be done in the background. Unfortunately
+                    // the current code layout and framework assumptions don't support
+                    // this. So for now, we could end up doing a bunch of i/o on main thread.
+                    v.startDragAndDrop(
+                            mClipper.getClipDataForDocuments(
+                                    mModel::getItemUri,
+                                    selection,
+                                    FileOperationService.OPERATION_COPY),
+                            new DragShadowBuilder(
+                                    getActivity(),
+                                    getDragTitle(selection),
+                                    getDragIcon(selection)),
+                            getDisplayState().stack.peek(),
+                            View.DRAG_FLAG_GLOBAL
+                                    | View.DRAG_FLAG_GLOBAL_URI_READ
+                                    | View.DRAG_FLAG_GLOBAL_URI_WRITE);
+
+                    return true;
+                }
+            };
+
+
+    private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
+
+    private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
+        @Override
+        public boolean onLongClick(View v) {
+            return mDragHelper.onLongClick(v);
+        }
+    };
+
+    private boolean canSelect(DocumentDetails doc) {
+        return canSelect(doc.getModelId());
+    }
+
+    private boolean canSelect(String modelId) {
+
+        // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost
+        // the same, and responsible for the same thing (whether to select or not).
+        final Cursor cursor = mModel.getItem(modelId);
+        if (cursor == null) {
+            Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId);
+            return false;
+        }
+
+        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        return mTuner.canSelectType(docMimeType, docFlags);
+    }
+
+    public static void showDirectory(
+            FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
+        create(fm, TYPE_NORMAL, root, doc, null, anim);
+    }
+
+    public static void showRecentsOpen(FragmentManager fm, int anim) {
+        create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
+    }
+
+    public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
+            String query) {
+        DirectoryFragment df = get(fm);
+
+        df.mQuery = query;
+        df.mRoot = root;
+        df.mDocument = doc;
+        df.mSearchMode =  query != null;
+        df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+    }
+
+    public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
+            String query) {
+        DirectoryFragment df = get(fm);
+        df.mType = type;
+        df.mQuery = query;
+        df.mRoot = root;
+        df.mDocument = doc;
+        df.mSearchMode =  query != null;
+        df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+    }
+
+    public static void create(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
+            String query, int anim) {
+        final Bundle args = new Bundle();
+        args.putInt(Shared.EXTRA_TYPE, type);
+        args.putParcelable(Shared.EXTRA_ROOT, root);
+        args.putParcelable(Shared.EXTRA_DOC, doc);
+        args.putString(Shared.EXTRA_QUERY, query);
+        args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
+
+        final FragmentTransaction ft = fm.beginTransaction();
+        AnimationView.setupAnimations(ft, anim, args);
+
+        final DirectoryFragment fragment = new DirectoryFragment();
+        fragment.setArguments(args);
+
+        ft.replace(getFragmentId(), fragment);
+        ft.commitAllowingStateLoss();
+    }
+
+    private static String buildStateKey(RootInfo root, DocumentInfo doc) {
+        final StringBuilder builder = new StringBuilder();
+        builder.append(root != null ? root.authority : "null").append(';');
+        builder.append(root != null ? root.rootId : "null").append(';');
+        builder.append(doc != null ? doc.documentId : "null");
+        return builder.toString();
+    }
+
+    public static @Nullable DirectoryFragment get(FragmentManager fm) {
+        // TODO: deal with multiple directories shown at once
+        Fragment fragment = fm.findFragmentById(getFragmentId());
+        return fragment instanceof DirectoryFragment
+                ? (DirectoryFragment) fragment
+                : null;
+    }
+
+    private static int getFragmentId() {
+        return R.id.container_directory;
+    }
+
+    @Override
+    public void onRefresh() {
+        // Remove thumbnail cache. We do this not because we're worried about stale thumbnails as it
+        // should be covered by last modified value we store in thumbnail cache, but rather to give
+        // the user a greater sense that contents are being reloaded.
+        ThumbnailCache cache = DocumentsApplication.getThumbnailCache(getContext());
+        String[] ids = mModel.getModelIds();
+        int numOfEvicts = Math.min(ids.length, CACHE_EVICT_LIMIT);
+        for (int i = 0; i < numOfEvicts; ++i) {
+            cache.removeUri(mModel.getItemUri(ids[i]));
+        }
+
+        // Trigger loading
+        getLoaderManager().restartLoader(LOADER_ID, null, this);
+    }
+
+    @Override
+    public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
+        Context context = getActivity();
+        State state = getDisplayState();
+
+        Uri contentsUri;
+        switch (mType) {
+            case TYPE_NORMAL:
+                contentsUri = mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
+                        mRoot.authority, mRoot.rootId, mQuery)
+                        : DocumentsContract.buildChildDocumentsUri(
+                                mDocument.authority, mDocument.documentId);
+                if (mTuner.managedModeEnabled()) {
+                    contentsUri = DocumentsContract.setManageMode(contentsUri);
+                }
+                return new DirectoryLoader(
+                        context, mType, mRoot, mDocument, contentsUri, state.userSortOrder,
+                        mSearchMode);
+            case TYPE_RECENT_OPEN:
+                final RootsCache roots = DocumentsApplication.getRootsCache(context);
+                return new RecentsLoader(context, roots, state);
+
+            default:
+                throw new IllegalStateException("Unknown type " + mType);
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+        if (!isAdded()) return;
+
+        if (mSearchMode) {
+            Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
+        }
+
+        State state = getDisplayState();
+
+        mAdapter.notifyDataSetChanged();
+        mModel.update(result);
+
+        state.derivedSortOrder = result.sortOrder;
+
+        updateLayout(state.derivedMode);
+
+        if (mRestoredSelection != null) {
+            mSelectionMgr.restoreSelection(mRestoredSelection);
+            // Note, we'll take care of cleaning up retained selection
+            // in the selection handler where we already have some
+            // specialized code to handle when selection was restored.
+        }
+
+        // Restore any previous instance state
+        final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
+        if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
+            getView().restoreHierarchyState(container);
+        } else if (mLastSortOrder != state.derivedSortOrder) {
+            // The derived sort order takes the user sort order into account, but applies
+            // directory-specific defaults when the user doesn't explicitly set the sort
+            // order. Scroll to the top if the sort order actually changed.
+            mRecView.smoothScrollToPosition(0);
+        }
+
+        mLastSortOrder = state.derivedSortOrder;
+
+        mTuner.onModelLoaded(mModel, mType, mSearchMode);
+
+        if (mRefreshLayout.isRefreshing()) {
+            new Handler().postDelayed(
+                    () -> mRefreshLayout.setRefreshing(false),
+                    REFRESH_SPINNER_DISMISS_DELAY);
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<DirectoryResult> loader) {
+        mModel.update(null);
+
+        mRefreshLayout.setRefreshing(false);
+    }
+  }