On-demand fetch for DocumentInfo when right-click on a root.

RootInfo has information regarding the whole root, but not scoped
narrowly enough for just the top-level document. We need the top level
DocumentInfo to know specifics such as is it writable. This does a fetch
of that when right clicking on a root, and defaults to a timeout of 1000
ms if it takes too long to answer back for whatever reason (Provider
went bad/non-responding, etc).

Bug: 31662523
Change-Id: I2278ea0171fb63a5783f9be619732c857558a1b6
diff --git a/src/com/android/documentsui/GetRootDocumentTask.java b/src/com/android/documentsui/GetRootDocumentTask.java
index 79468ba..e56b110 100644
--- a/src/com/android/documentsui/GetRootDocumentTask.java
+++ b/src/com/android/documentsui/GetRootDocumentTask.java
@@ -33,13 +33,14 @@
  * {@link DocumentInfo} of its root document and call supplied callback to handle the
  * {@link DocumentInfo}.
  */
-public class GetRootDocumentTask extends CheckedTask<Void, DocumentInfo> {
+public class GetRootDocumentTask extends TimeoutTask<Void, DocumentInfo> {
 
     private final static String TAG = "GetRootDocumentTask";
 
     private final RootInfo mRootInfo;
     private final Context mContext;
     private final Consumer<DocumentInfo> mCallback;
+    private boolean mForceCallback;
 
     public GetRootDocumentTask(
             RootInfo rootInfo, Activity activity, Consumer<DocumentInfo> callback) {
@@ -59,6 +60,10 @@
         mCallback = callback;
     }
 
+    public void setForceCallback(boolean forceCallback) {
+        mForceCallback = forceCallback;
+    }
+
     @Override
     public @Nullable DocumentInfo run(Void... rootInfo) {
         return mRootInfo.getRootDocumentBlocking(mContext);
@@ -66,10 +71,13 @@
 
     @Override
     public void finish(@Nullable DocumentInfo documentInfo) {
-        if (documentInfo != null) {
+        if (documentInfo == null) {
+            Log.e(TAG,
+                    "Cannot find document info for root: " + mRootInfo + " in the given timeout");
+        }
+
+        if (documentInfo != null || mForceCallback) {
             mCallback.accept(documentInfo);
-        } else {
-            Log.e(TAG, "Cannot find document info for root: " + mRootInfo);
         }
     }
 }
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index 56f4214..c6e9514 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -22,6 +22,7 @@
 import android.view.MenuItem;
 import android.view.View;
 
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Menus;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
@@ -181,14 +182,14 @@
     /**
      * @see RootsFragment#onCreateContextMenu
      */
-    public void updateRootContextMenu(Menu menu, RootInfo root) {
+    public void updateRootContextMenu(Menu menu, RootInfo root, DocumentInfo docInfo) {
         MenuItem eject = menu.findItem(R.id.menu_eject_root);
         MenuItem pasteInto = menu.findItem(R.id.menu_paste_into_folder);
         MenuItem openInNewWindow = menu.findItem(R.id.menu_open_in_new_window);
         MenuItem settings = menu.findItem(R.id.menu_settings);
 
         updateEject(eject, root);
-        updatePasteInto(pasteInto, root);
+        updatePasteInto(pasteInto, root, docInfo);
         updateOpenInNewWindow(openInNewWindow, root);
         updateSettings(settings, root);
     }
@@ -262,7 +263,7 @@
         pasteInto.setVisible(false);
     }
 
-    protected void updatePasteInto(MenuItem pasteInto, RootInfo root) {
+    protected void updatePasteInto(MenuItem pasteInto, RootInfo root, DocumentInfo docInfo) {
         pasteInto.setVisible(false);
     }
 
diff --git a/src/com/android/documentsui/TimeoutTask.java b/src/com/android/documentsui/TimeoutTask.java
new file mode 100644
index 0000000..1d36b48
--- /dev/null
+++ b/src/com/android/documentsui/TimeoutTask.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.annotation.CallSuper;
+import android.os.AsyncTask;
+import android.os.Handler;
+
+import com.android.documentsui.base.CheckedTask;
+import com.android.documentsui.base.DocumentInfo;
+
+/**
+ * A {@link CheckedTask} that takes  and query SAF to obtain the
+ * {@link DocumentInfo} of its root document and call supplied callback to handle the
+ * {@link DocumentInfo}.
+ */
+public abstract class TimeoutTask<Input, Output> extends CheckedTask<Input, Output> {
+    private static final int DEFAULT_TIMEOUT = -1;
+
+    private long mTimeout = DEFAULT_TIMEOUT;
+
+    public TimeoutTask(Check check) {
+        super(check);
+    }
+
+    public void setTimeout(long timeout) {
+        mTimeout = timeout;
+    }
+
+    @CallSuper
+    @Override
+    protected void prepare() {
+        if (mTimeout < 0) {
+            return;
+        }
+
+        Handler handler = new Handler();
+        handler.postDelayed(() -> {
+            if (getStatus() == AsyncTask.Status.RUNNING) {
+                cancel(true);
+                this.finish(null);
+            }
+        }, mTimeout);
+    }
+
+}
diff --git a/src/com/android/documentsui/manager/MenuManager.java b/src/com/android/documentsui/manager/MenuManager.java
index bc81c3e..f898b6d 100644
--- a/src/com/android/documentsui/manager/MenuManager.java
+++ b/src/com/android/documentsui/manager/MenuManager.java
@@ -24,6 +24,7 @@
 
 import com.android.documentsui.R;
 import com.android.documentsui.SearchViewManager;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 
@@ -145,9 +146,11 @@
     }
 
     @Override
-    protected void updatePasteInto(MenuItem pasteInto, RootInfo root) {
-        // TODO(b/31658763): Check the root document as well.
-        pasteInto.setEnabled(root.supportsCreate() && mDirDetails.hasItemsToPaste());
+    protected void updatePasteInto(MenuItem pasteInto, RootInfo root, DocumentInfo docInfo) {
+        pasteInto.setEnabled(root.supportsCreate()
+                && docInfo != null
+                && docInfo.isCreateSupported()
+                && mDirDetails.hasItemsToPaste());
     }
 
     @Override
diff --git a/src/com/android/documentsui/sidebar/RootItem.java b/src/com/android/documentsui/sidebar/RootItem.java
index 4410b46..97ec89f 100644
--- a/src/com/android/documentsui/sidebar/RootItem.java
+++ b/src/com/android/documentsui/sidebar/RootItem.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui.sidebar;
 
+import android.annotation.Nullable;
 import android.content.ClipData;
 import android.content.Context;
 import android.provider.DocumentsProvider;
@@ -31,6 +32,7 @@
 import com.android.documentsui.ActionHandler;
 import com.android.documentsui.MenuManager;
 import com.android.documentsui.R;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 
 /**
@@ -40,6 +42,7 @@
     private static final String STRING_ID_FORMAT = "RootItem{%s/%s}";
 
     public final RootInfo root;
+    public @Nullable DocumentInfo docInfo;
 
     private final ActionHandler mActionHandler;
 
@@ -110,6 +113,6 @@
     @Override
     void createContextMenu(Menu menu, MenuInflater inflater, MenuManager menuManager) {
         inflater.inflate(R.menu.root_context_menu, menu);
-        menuManager.updateRootContextMenu(menu, root);
+        menuManager.updateRootContextMenu(menu, root, docInfo);
     }
 }
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 1cbed8e..7b9c99c 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -29,7 +29,9 @@
 import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.DragEvent;
@@ -49,9 +51,11 @@
 import com.android.documentsui.ActionHandler;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.GetRootDocumentTask;
 import com.android.documentsui.ItemDragListener;
 import com.android.documentsui.R;
 import com.android.documentsui.base.BooleanConsumer;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.RootInfo;
@@ -74,6 +78,7 @@
 
     private static final String TAG = "RootsFragment";
     private static final String EXTRA_INCLUDE_APPS = "includeApps";
+    private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500;
 
     private final OnDragListener mDragListener = new ItemDragListener<RootsFragment>(this) {
         @Override
@@ -143,21 +148,53 @@
         // All other motion events will then get passed to OnItemClickListener.
         mList.setOnGenericMotionListener(
                 new OnGenericMotionListener() {
-            @Override
-            public boolean onGenericMotion(View v, MotionEvent event) {
-                if (Events.isMouseEvent(event)
-                        && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
-                    getBaseActivity().getMenuManager().showContextMenu(
-                            RootsFragment.this, v, event.getX(), event.getY());
-                    return true;
-                }
-                return false;
+                    @Override
+                    public boolean onGenericMotion(View v, MotionEvent event) {
+                        if (Events.isMouseEvent(event)
+                                && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
+                            int x = (int) event.getX();
+                            int y = (int) event.getY();
+                            return onRightClick(v, x, y, () -> {
+                                getBaseActivity().getMenuManager()
+                                        .showContextMenu(RootsFragment.this, v, x, y);
+                            });
+                        }
+                        return false;
             }
         });
         mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
         return view;
     }
 
+    private boolean onRightClick(View v, int x, int y, Runnable callback) {
+        int pos = mList.pointToPosition(x, y);
+        final Item item = mAdapter.getItem(pos);
+        if (!(item instanceof RootItem)) {
+            return false;
+        }
+        final RootItem rootItem = (RootItem) item;
+
+        if (!rootItem.root.supportsCreate()) {
+            // If a read-only root, no need to see if top level is writable (it's not)
+            callback.run();
+            return true;
+        }
+        // We need to start a GetRootDocumentTask so we can know whether items can be directly
+        // pasted into root
+        GetRootDocumentTask task = new GetRootDocumentTask(
+                rootItem.root,
+                getBaseActivity(),
+                (DocumentInfo doc) -> {
+                    rootItem.docInfo = doc;
+                    callback.run();
+                });
+        task.setTimeout(CONTEXT_MENU_ITEM_TIMEOUT);
+        task.setForceCallback(true);
+        task.executeOnExecutor(getBaseActivity().getExecutorForCurrentDirectory());
+
+        return true;
+    }
+
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
diff --git a/tests/unit/com/android/documentsui/manager/MenuManagerTest.java b/tests/unit/com/android/documentsui/manager/MenuManagerTest.java
index 8049203..cd1c247 100644
--- a/tests/unit/com/android/documentsui/manager/MenuManagerTest.java
+++ b/tests/unit/com/android/documentsui/manager/MenuManagerTest.java
@@ -17,14 +17,15 @@
 package com.android.documentsui.manager;
 
 import static junit.framework.Assert.assertEquals;
-
 import static org.junit.Assert.assertTrue;
 
+import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.testing.TestDirectoryDetails;
@@ -65,6 +66,7 @@
     private TestDirectoryDetails dirDetails;
     private TestSearchViewManager testSearchManager;
     private RootInfo testRootInfo;
+    private DocumentInfo testDocInfo;
     private State state = new State();
     private MenuManager mgr;
 
@@ -100,6 +102,7 @@
         mgr = new MenuManager(testSearchManager, state, dirDetails);
 
         testRootInfo = new RootInfo();
+        testDocInfo = new DocumentInfo();
     }
 
     @Test
@@ -364,7 +367,7 @@
     public void testRootContextMenu() {
         testRootInfo.flags = Root.FLAG_SUPPORTS_CREATE;
 
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         eject.assertInvisible();
 
@@ -381,7 +384,7 @@
     @Test
     public void testRootContextMenu_hasRootSettings() {
         testRootInfo.flags = Root.FLAG_HAS_SETTINGS;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         settings.assertEnabled();
     }
@@ -389,7 +392,7 @@
     @Test
     public void testRootContextMenu_nonWritableRoot() {
         dirDetails.hasItemsToPaste = true;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         pasteInto.assertVisible();
         pasteInto.assertDisabled();
@@ -398,17 +401,29 @@
     @Test
     public void testRootContextMenu_nothingToPaste() {
         testRootInfo.flags = Root.FLAG_SUPPORTS_CREATE;
+        testDocInfo.flags = Document.FLAG_DIR_SUPPORTS_CREATE;
         dirDetails.hasItemsToPaste = false;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         pasteInto.assertVisible();
         pasteInto.assertDisabled();
     }
 
     @Test
+    public void testRootContextMenu_pasteIntoWritableRoot() {
+        testRootInfo.flags = Root.FLAG_SUPPORTS_CREATE;
+        testDocInfo.flags = Document.FLAG_DIR_SUPPORTS_CREATE;
+        dirDetails.hasItemsToPaste = true;
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
+
+        pasteInto.assertVisible();
+        pasteInto.assertEnabled();
+    }
+
+    @Test
     public void testRootContextMenu_eject() {
         testRootInfo.flags = Root.FLAG_SUPPORTS_EJECT;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         eject.assertEnabled();
     }
@@ -417,7 +432,7 @@
     public void testRootContextMenu_ejectInProcess() {
         testRootInfo.flags = Root.FLAG_SUPPORTS_EJECT;
         testRootInfo.ejecting = true;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         eject.assertDisabled();
     }
diff --git a/tests/unit/com/android/documentsui/picker/MenuManagerTest.java b/tests/unit/com/android/documentsui/picker/MenuManagerTest.java
index f23e0ac..bf9f5cf 100644
--- a/tests/unit/com/android/documentsui/picker/MenuManagerTest.java
+++ b/tests/unit/com/android/documentsui/picker/MenuManagerTest.java
@@ -18,7 +18,6 @@
 
 import static com.android.documentsui.base.State.ACTION_CREATE;
 import static com.android.documentsui.base.State.ACTION_OPEN;
-
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -27,6 +26,7 @@
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.testing.TestDirectoryDetails;
@@ -67,6 +67,7 @@
     private TestSearchViewManager testSearchManager;
     private State state = new State();
     private RootInfo testRootInfo;
+    private DocumentInfo testDocInfo;
     private MenuManager mgr;
 
     @Before
@@ -97,6 +98,7 @@
         mgr = new MenuManager(testSearchManager, state, dirDetails);
 
         testRootInfo = new RootInfo();
+        testDocInfo = new DocumentInfo();
         state.action = ACTION_CREATE;
         state.allowMultiple = true;
     }
@@ -258,7 +260,7 @@
 
     @Test
     public void testRootContextMenu() {
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         eject.assertInvisible();
         openInNewWindow.assertInvisible();
@@ -269,7 +271,7 @@
     @Test
     public void testRootContextMenu_hasRootSettings() {
         testRootInfo.flags = Root.FLAG_HAS_SETTINGS;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         settings.assertInvisible();
     }
@@ -277,7 +279,7 @@
     @Test
     public void testRootContextMenu_nonWritableRoot() {
         dirDetails.hasItemsToPaste = true;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         pasteInto.assertInvisible();
     }
@@ -286,7 +288,7 @@
     public void testRootContextMenu_nothingToPaste() {
         testRootInfo.flags = Root.FLAG_SUPPORTS_CREATE;
         dirDetails.hasItemsToPaste = false;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         pasteInto.assertInvisible();
     }
@@ -294,7 +296,7 @@
     @Test
     public void testRootContextMenu_canEject() {
         testRootInfo.flags = Root.FLAG_SUPPORTS_EJECT;
-        mgr.updateRootContextMenu(testMenu, testRootInfo);
+        mgr.updateRootContextMenu(testMenu, testRootInfo, testDocInfo);
 
         eject.assertInvisible();
     }