Add copy/paste menus to StandaloneMode.

Execute encoding/decoding of clipboard data in background...
was seeing a lot of red flashes for large file selections.
Move the majority of the Clipboard management code into a
shared DocumentClipper class.

Hide the copy-to and move-to menus in same.

BUG=20915675
Change-Id: Iff59e846afe6f1e90e6be816f5860d8b7efe3ae1
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 8b86423..96cfa33 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -29,6 +29,7 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.Fragment;
@@ -90,6 +91,7 @@
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 import com.android.documentsui.model.RootInfo;
+import com.android.internal.util.Preconditions;
 
 import com.google.android.collect.Lists;
 
@@ -146,6 +148,9 @@
 
     private final int mLoaderId = 42;
 
+    private FragmentTuner mFragmentTuner;
+    private DocumentClipper mClipper;
+
     public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
         show(fm, TYPE_NORMAL, root, doc, null, anim);
     }
@@ -268,6 +273,9 @@
         mType = getArguments().getInt(EXTRA_TYPE);
         mStateKey = buildStateKey(root, doc);
 
+        mFragmentTuner = pickFragmentTuner(state);
+        mClipper = new DocumentClipper(context);
+
         if (mType == TYPE_RECENT_OPEN) {
             // Hide titles when showing recents for picking images/videos
             mHideGridTitles = MimePredicate.mimeMatches(
@@ -499,29 +507,17 @@
 
         @Override
         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-            final State state = getDisplayState(DirectoryFragment.this);
-
-            final MenuItem open = menu.findItem(R.id.menu_open);
-            final MenuItem share = menu.findItem(R.id.menu_share);
-            final MenuItem delete = menu.findItem(R.id.menu_delete);
-            final MenuItem copy = menu.findItem(R.id.menu_copy);
-            final MenuItem move = menu.findItem(R.id.menu_move);
-
-            final boolean manageOrBrowse = (state.action == ACTION_MANAGE
-                    || state.action == ACTION_BROWSE || state.action == ACTION_BROWSE_ALL);
-
-            open.setVisible(!manageOrBrowse);
-            share.setVisible(manageOrBrowse);
-            delete.setVisible(manageOrBrowse);
-            // Disable copying from the Recents view.
-            copy.setVisible(manageOrBrowse && mType != TYPE_RECENT_OPEN);
-            move.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_move", false));
+            // Delegate update logic to our owning action, since specialized
+            // logic is desired.
+            mFragmentTuner.updateActionMenu(menu, mType);
             return true;
         }
 
         @Override
-        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            final List<DocumentInfo> docs = getSelectedDocuments();
+        public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
+
+            // TODO: Call `getSelectedDocuments` in an AsyncTask to avoid UI jank.
+            List<DocumentInfo> docs = getSelectedDocuments();
 
             final int id = item.getItemId();
             if (id == R.id.menu_open) {
@@ -539,16 +535,21 @@
                 mode.finish();
                 return true;
 
-            } else if (id == R.id.menu_copy) {
+            } else if (id == R.id.menu_copy_to) {
                 onTransferDocuments(docs, CopyService.TRANSFER_MODE_COPY);
                 mode.finish();
                 return true;
 
-            } else if (id == R.id.menu_move) {
+            } else if (id == R.id.menu_move_to) {
                 onTransferDocuments(docs, CopyService.TRANSFER_MODE_MOVE);
                 mode.finish();
                 return true;
 
+            } else if (id == R.id.menu_copy_to_clipboard) {
+                copySelectedToClipboard();
+                mode.finish();
+                return true;
+
             } else if (id == R.id.menu_select_all) {
                 selectAllFiles();
                 return true;
@@ -1210,13 +1211,20 @@
         return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
     }
 
-    public List<DocumentInfo> getSelectedDocuments() {
-        final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
-        final List<DocumentInfo> docs = Lists.newArrayList();
-        final int size = checked.size();
+    private @NonNull List<DocumentInfo> getSelectedDocuments() {
+        return getItemsAsDocuments(mCurrentView.getCheckedItemPositions().clone());
+    }
+
+    private List<DocumentInfo> getItemsAsDocuments(SparseBooleanArray items) {
+        if (items == null || items.size() == 0) {
+            return new ArrayList<>(0);
+        }
+
+        final List<DocumentInfo> docs =  new ArrayList<>(items.size());
+        final int size = items.size();
         for (int i = 0; i < size; i++) {
-            if (checked.valueAt(i)) {
-                final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
+            if (items.valueAt(i)) {
+                final Cursor cursor = mAdapter.getItem(items.keyAt(i));
                 final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
                 docs.add(doc);
             }
@@ -1224,65 +1232,61 @@
         return docs;
     }
 
-    private void copyFromClipData(ClipData clipData) {
-        copyFromClipData(
-                clipData,
-                ((BaseActivity)getActivity()).getCurrentDirectory());
+    private void copyFromClipboard() {
+        new AsyncTask<Void, Void, List<DocumentInfo>>() {
+
+            @Override
+            protected List<DocumentInfo> doInBackground(Void... params) {
+                return mClipper.getClippedDocuments();
+            }
+
+            @Override
+            protected void onPostExecute(List<DocumentInfo> docs) {
+                DocumentInfo destination =
+                        ((BaseActivity) getActivity()).getCurrentDirectory();
+                copyDocuments(docs, destination);
+            }
+        }.execute();
     }
 
-    private void copyFromClipData(ClipData clipData, DocumentInfo dstDir) {
-        final List<DocumentInfo> srcDocs = getDocumentsFromClipData(clipData);
+    private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
+        Preconditions.checkNotNull(clipData);
+        new AsyncTask<Void, Void, List<DocumentInfo>>() {
 
-        if (!canCopy(srcDocs, dstDir)) {
-            Toast.makeText(getActivity(), R.string.clipboard_files_cannot_paste, Toast.LENGTH_SHORT).show();
+            @Override
+            protected List<DocumentInfo> doInBackground(Void... params) {
+                return mClipper.getDocumentsFromClipData(clipData);
+            }
+
+            @Override
+            protected void onPostExecute(List<DocumentInfo> docs) {
+                copyDocuments(docs, destination);
+            }
+        }.execute();
+    }
+
+    private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
+        if (!canCopy(docs, destination)) {
+            Toast.makeText(
+                    getActivity(),
+                    R.string.clipboard_files_cannot_paste, Toast.LENGTH_SHORT).show();
             return;
         }
 
-        if (srcDocs.isEmpty()) {
+        if (docs.isEmpty()) {
             return;
         }
 
-        final DocumentStack curStack = getDisplayState(this).stack;
+        final DocumentStack curStack = getDisplayState(DirectoryFragment.this).stack;
         DocumentStack tmpStack = new DocumentStack();
-        if (dstDir != null) {
-            tmpStack.push(dstDir);
+        if (destination != null) {
+            tmpStack.push(destination);
             tmpStack.addAll(curStack);
         } else {
             tmpStack = curStack;
         }
 
-        CopyService.start(getActivity(), srcDocs, tmpStack, CopyService.TRANSFER_MODE_COPY);
-    }
-
-    private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
-        final List<DocumentInfo> srcDocs = Lists.newArrayList();
-
-        Context context = getActivity();
-        final ContentResolver resolver = context.getContentResolver();
-
-        int itemCount = clipData.getItemCount();
-        for (int i = 0; i < itemCount; ++i) {
-            ClipData.Item item = clipData.getItemAt(i);
-            Uri itemUri = item.getUri();
-            if (itemUri != null && DocumentsContract.isDocumentUri(context, itemUri)) {
-                ContentProviderClient client = null;
-                Cursor cursor = null;
-                try {
-                    client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                            resolver, itemUri.getAuthority());
-                    cursor = client.query(itemUri, null, null, null, null);
-                    cursor.moveToPosition(0);
-                    srcDocs.add(DocumentInfo.fromCursor(cursor, itemUri.getAuthority()));
-                } catch (Exception e) {
-                    Log.e(TAG, e.getMessage());
-                } finally {
-                    IoUtils.closeQuietly(cursor);
-                    ContentProviderClient.releaseQuietly(client);
-                }
-            }
-        }
-
-        return srcDocs;
+        CopyService.start(getActivity(), docs, tmpStack, CopyService.TRANSFER_MODE_COPY);
     }
 
     private ClipData getClipDataFromDocuments(List<DocumentInfo> docs) {
@@ -1304,22 +1308,40 @@
         return clipData;
     }
 
-    void copyToClipboard() {
-        ClipboardManager clipboard = getClipboardManager();
-        List<DocumentInfo> docs = getSelectedDocuments();
-        ClipData data = getClipDataFromDocuments(docs);
-        clipboard.setPrimaryClip(data);
+    void copySelectedToClipboard() {
+        // ListView returns a reference to its internal selection container,
+        // which will get cleared when we cancel action mode. So we
+        // make a defensive clone here.
+        //
+        // Furthermore, we don't want to call this from within the async task for
+        // basically the same reason...when mode is cancelled, selection is cleared.
+        final SparseBooleanArray selection = mCurrentView.getCheckedItemPositions().clone();
 
-        Activity activity = getActivity();
-        Toast.makeText(activity,
-                activity.getResources().getQuantityString(
-                        R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
-                Toast.LENGTH_SHORT).show();
+        new AsyncTask<Void, Void, List<DocumentInfo>>() {
+            protected List<DocumentInfo> doInBackground(Void... params) {
+                List<DocumentInfo> docs = getItemsAsDocuments(selection);
+                if (!docs.isEmpty()) {
+                    mClipper.clipDocuments(docs);
+                }
+                return docs;
+            };
+            protected void onPostExecute(List<DocumentInfo> docs) {
+                if (docs.isEmpty()) {
+                    Log.i(TAG, "Skipped populating clipboard with empty selection.");
+                    return;
+                }
+                Activity activity = getActivity();
+                Toast.makeText(activity,
+                        activity.getResources().getQuantityString(
+                                R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
+                                Toast.LENGTH_SHORT).show();
+            };
+        }.execute();
     }
 
     void pasteFromClipboard() {
-        ClipboardManager clipboard = getClipboardManager();
-        copyFromClipData(clipboard.getPrimaryClip());
+        copyFromClipboard();
+        getActivity().invalidateOptionsMenu();
     }
 
     private ClipboardManager getClipboardManager() {
@@ -1489,4 +1511,71 @@
             mShadow.draw(canvas);
         }
     }
+
+    private FragmentTuner pickFragmentTuner(final State state) {
+        return state.action == ACTION_BROWSE_ALL
+                ? new StandaloneTuner()
+                : new DefaultTuner(state);
+    }
+
+    /**
+     * Interface for specializing the Fragment for the "host" Activity.
+     * Feel free to expand the role of this class to handle other specializations.
+     */
+    private interface FragmentTuner {
+        void updateActionMenu(Menu menu, int dirType);
+    }
+
+    /**
+     * Provides support for Platform specific specializations of DirectoryFragment.
+     */
+    private static final class DefaultTuner implements FragmentTuner {
+
+        private final State mState;
+
+        public DefaultTuner(State state) {
+            mState = state;
+        }
+
+        @Override
+        public void updateActionMenu(Menu menu, int dirType) {
+            Preconditions.checkState(mState.action != ACTION_BROWSE_ALL);
+
+            final MenuItem open = menu.findItem(R.id.menu_open);
+            final MenuItem share = menu.findItem(R.id.menu_share);
+            final MenuItem delete = menu.findItem(R.id.menu_delete);
+            final MenuItem copyTo = menu.findItem(R.id.menu_copy_to);
+            final MenuItem moveTo = menu.findItem(R.id.menu_move_to);
+            final MenuItem copyToClipboard = menu.findItem(R.id.menu_copy_to_clipboard);
+
+            final boolean manageOrBrowse = (mState.action == ACTION_MANAGE
+                    || mState.action == ACTION_BROWSE);
+
+            open.setVisible(!manageOrBrowse);
+            share.setVisible(manageOrBrowse);
+            delete.setVisible(manageOrBrowse);
+            // Disable copying from the Recents view.
+            copyTo.setVisible(manageOrBrowse && dirType != TYPE_RECENT_OPEN);
+            moveTo.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_move", false));
+
+            // Only shown in standalone mode.
+            copyToClipboard.setVisible(false);
+        }
+    }
+
+    /**
+     * Provides support for Standalone specific specializations of DirectoryFragment.
+     */
+    private static final class StandaloneTuner implements FragmentTuner {
+        @Override
+        public void updateActionMenu(Menu menu, int dirType) {
+            menu.findItem(R.id.menu_share).setVisible(true);
+            menu.findItem(R.id.menu_delete).setVisible(true);
+            menu.findItem(R.id.menu_copy_to_clipboard).setVisible(true);
+
+            menu.findItem(R.id.menu_open).setVisible(false);
+            menu.findItem(R.id.menu_copy_to).setVisible(false);
+            menu.findItem(R.id.menu_move_to).setVisible(false);
+        }
+    }
 }