Do more work off main thread.

Create List<DocumentInfo> in asynctask to avoid UI jank.

Change-Id: I0a1274b4903c48332c2593714a2e7972b7b2686c
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 96cfa33..8519bef 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -516,37 +516,39 @@
         @Override
         public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
 
-            // TODO: Call `getSelectedDocuments` in an AsyncTask to avoid UI jank.
-            List<DocumentInfo> docs = getSelectedDocuments();
+            // 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.
+            final SparseBooleanArray selected = mCurrentView.getCheckedItemPositions().clone();
 
             final int id = item.getItemId();
             if (id == R.id.menu_open) {
-                BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
+                openDocuments(selected);
                 mode.finish();
                 return true;
 
             } else if (id == R.id.menu_share) {
-                onShareDocuments(docs);
+                shareDocuments(selected);
                 mode.finish();
                 return true;
 
             } else if (id == R.id.menu_delete) {
-                onDeleteDocuments(docs);
+                deleteDocuments(selected);
                 mode.finish();
                 return true;
 
             } else if (id == R.id.menu_copy_to) {
-                onTransferDocuments(docs, CopyService.TRANSFER_MODE_COPY);
+                transferDocuments(selected, CopyService.TRANSFER_MODE_COPY);
                 mode.finish();
                 return true;
 
             } else if (id == R.id.menu_move_to) {
-                onTransferDocuments(docs, CopyService.TRANSFER_MODE_MOVE);
+                transferDocuments(selected, CopyService.TRANSFER_MODE_MOVE);
                 mode.finish();
                 return true;
 
             } else if (id == R.id.menu_copy_to_clipboard) {
-                copySelectedToClipboard();
+                copySelectionToClipboard(selected);
                 mode.finish();
                 return true;
 
@@ -602,82 +604,103 @@
         }
     };
 
-    private void onShareDocuments(List<DocumentInfo> docs) {
-        Intent intent;
-
-        // Filter out directories - those can't be shared.
-        List<DocumentInfo> docsForSend = Lists.newArrayList();
-        for (DocumentInfo doc: docs) {
-            if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
-                docsForSend.add(doc);
+    private void openDocuments(final SparseBooleanArray selected) {
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                // TODO: Implement support in standalone for opening multiple docs.
+                BaseActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
             }
-        }
-
-        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 = Lists.newArrayList();
-            final ArrayList<Uri> uris = Lists.newArrayList();
-            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 void onDeleteDocuments(List<DocumentInfo> docs) {
+    private void shareDocuments(final SparseBooleanArray selected) {
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                Intent intent;
+
+                // Filter out directories - those can't be shared.
+                List<DocumentInfo> docsForSend = Lists.newArrayList();
+                for (DocumentInfo doc: docs) {
+                    if (!Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
+                        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 = Lists.newArrayList();
+                    final ArrayList<Uri> uris = Lists.newArrayList();
+                    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 void deleteDocuments(final SparseBooleanArray selected) {
         final Context context = getActivity();
         final ContentResolver resolver = context.getContentResolver();
 
-        boolean hadTrouble = false;
-        for (DocumentInfo doc : docs) {
-            if (!doc.isDeleteSupported()) {
-                Log.w(TAG, "Skipping " + doc);
-                hadTrouble = true;
-                continue;
-            }
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                boolean hadTrouble = false;
+                for (DocumentInfo doc : docs) {
+                    if (!doc.isDeleteSupported()) {
+                        Log.w(TAG, "Skipping " + doc);
+                        hadTrouble = true;
+                        continue;
+                    }
 
-            ContentProviderClient client = null;
-            try {
-                client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                        resolver, doc.derivedUri.getAuthority());
-                DocumentsContract.deleteDocument(client, doc.derivedUri);
-            } catch (Exception e) {
-                Log.w(TAG, "Failed to delete " + doc);
-                hadTrouble = true;
-            } finally {
-                ContentProviderClient.releaseQuietly(client);
-            }
-        }
+                    ContentProviderClient client = null;
+                    try {
+                        client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                                resolver, doc.derivedUri.getAuthority());
+                        DocumentsContract.deleteDocument(client, doc.derivedUri);
+                    } catch (Exception e) {
+                        Log.w(TAG, "Failed to delete " + doc);
+                        hadTrouble = true;
+                    } finally {
+                        ContentProviderClient.releaseQuietly(client);
+                    }
+                }
 
-        if (hadTrouble) {
-            Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
-        }
+                if (hadTrouble) {
+                    Toast.makeText(
+                            context,
+                            R.string.toast_failed_delete,
+                            Toast.LENGTH_SHORT).show();
+                }
+            }
+        }.execute(selected);
     }
 
-    private void onTransferDocuments(List<DocumentInfo> docs, int mode) {
-        getDisplayState(this).selectedDocumentsForCopy = docs;
-
+    private void transferDocuments(final SparseBooleanArray selected, final int mode) {
         // 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(
@@ -685,16 +708,24 @@
                 Uri.EMPTY,
                 getActivity(),
                 DocumentsActivity.class);
-        boolean directoryCopy = false;
-        for (DocumentInfo info : docs) {
-            if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
-                directoryCopy = true;
-                break;
+
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                getDisplayState(DirectoryFragment.this).selectedDocumentsForCopy = docs;
+
+                boolean directoryCopy = false;
+                for (DocumentInfo info : docs) {
+                    if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
+                        directoryCopy = true;
+                        break;
+                    }
+                }
+                intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
+                intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode);
+                startActivityForResult(intent, REQUEST_COPY_DESTINATION);
             }
-        }
-        intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
-        intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode);
-        startActivityForResult(intent, REQUEST_COPY_DESTINATION);
+        }.execute(selected);
     }
 
     private static State getDisplayState(Fragment fragment) {
@@ -1309,34 +1340,21 @@
     }
 
     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();
+        copySelectionToClipboard(mCurrentView.getCheckedItemPositions().clone());
+    }
 
-        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;
-                }
+    void copySelectionToClipboard(SparseBooleanArray selected) {
+        new GetDocumentsTask() {
+            @Override
+            void onDocumentsReady(List<DocumentInfo> docs) {
+                mClipper.clipDocuments(docs);
                 Activity activity = getActivity();
                 Toast.makeText(activity,
                         activity.getResources().getQuantityString(
                                 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
                                 Toast.LENGTH_SHORT).show();
-            };
-        }.execute();
+            }
+        }.execute(selected);
     }
 
     void pasteFromClipboard() {
@@ -1344,10 +1362,6 @@
         getActivity().invalidateOptionsMenu();
     }
 
-    private ClipboardManager getClipboardManager() {
-        return (ClipboardManager)getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
-    }
-
     /**
      * Returns true if the list of files can be copied to destination. Note that this
      * is a policy check only. Currently the method does not attempt to verify
@@ -1527,6 +1541,26 @@
     }
 
     /**
+     * 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<SparseBooleanArray, Void, List<DocumentInfo>> {
+        @Override
+        protected final List<DocumentInfo> doInBackground(SparseBooleanArray... selected) {
+            return getItemsAsDocuments(selected[0]);
+        }
+
+        @Override
+        protected final void onPostExecute(List<DocumentInfo> docs) {
+            onDocumentsReady(docs);
+        }
+
+        abstract void onDocumentsReady(List<DocumentInfo> docs);
+    }
+
+    /**
      * Provides support for Platform specific specializations of DirectoryFragment.
      */
     private static final class DefaultTuner implements FragmentTuner {