Merge "Make RecentsLoader load on a per-authority basis instead of per-root." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
new file mode 100644
index 0000000..65f324e
--- /dev/null
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -0,0 +1,104 @@
+/*
+ * 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.app.Activity;
+import android.content.ClipData;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.Parcelable;
+
+import com.android.documentsui.base.BooleanConsumer;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.dirlist.DocumentDetails;
+import com.android.documentsui.manager.LauncherActivity;
+import com.android.documentsui.sidebar.EjectRootTask;
+
+/**
+ * Provides support for specializing the actions (viewDocument etc.) to the host activity.
+ */
+public abstract class AbstractActionHandler<T extends Activity> implements ActionHandler {
+
+    protected final T mActivity;
+
+    public AbstractActionHandler(T activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public void openSettings(RootInfo root) {
+        throw new UnsupportedOperationException("Can't open settings.");
+    }
+
+    @Override
+    public void ejectRoot(RootInfo root, BooleanConsumer listener) {
+        new EjectRootTask(
+                mActivity.getContentResolver(),
+                root.authority,
+                root.rootId,
+                listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
+    }
+
+    @Override
+    public void openRoot(ResolveInfo app) {
+        throw new UnsupportedOperationException("Can't open an app.");
+    }
+
+    @Override
+    public void showAppDetails(ResolveInfo info) {
+        throw new UnsupportedOperationException("Can't show app details.");
+    }
+
+    @Override
+    public boolean dropOn(ClipData data, RootInfo root) {
+        throw new UnsupportedOperationException("Can't open an app.");
+    }
+
+    @Override
+    public void openInNewWindow(DocumentStack path) {
+        Metrics.logUserAction(mActivity, Metrics.USER_ACTION_NEW_WINDOW);
+
+        Intent intent = LauncherActivity.createLaunchIntent(mActivity);
+        intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path);
+
+        // Multi-window necessitates we pick how we are launched.
+        // By default we'd be launched in-place above the existing app.
+        // By setting launch-to-side ActivityManager will open us to side.
+        if (mActivity.isInMultiWindowMode()) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
+        }
+
+        mActivity.startActivity(intent);
+    }
+
+    @Override
+    public void pasteIntoFolder(RootInfo root) {
+        throw new UnsupportedOperationException("Can't paste into folder.");
+    }
+
+    @Override
+    public boolean viewDocument(DocumentDetails doc) {
+        throw new UnsupportedOperationException("Direct view not supported!");
+    }
+
+    @Override
+    public boolean previewDocument(DocumentDetails doc) {
+        throw new UnsupportedOperationException("Preview not supported!");
+    }
+}
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 6e8f90b..dddd374 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -19,102 +19,39 @@
 import android.content.ClipData;
 import android.content.pm.ResolveInfo;
 
-import com.android.documentsui.base.CheckedTask.Check;
-import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.BooleanConsumer;
+import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.clipping.DocumentClipper;
 import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.sidebar.EjectRootTask;
 
-import java.util.function.BooleanSupplier;
-import java.util.function.Consumer;
+public interface ActionHandler {
 
-/**
- * Provides support for specializing the actions (viewDocument etc.) to the host activity.
- */
-public abstract class ActionHandler<T extends BaseActivity> {
-
-    protected T mActivity;
-
-    public ActionHandler(T activity) {
-        mActivity = activity;
-    }
-
-    public void openSettings(RootInfo root) {
-        throw new UnsupportedOperationException("Can't open settings.");
-    }
+    void openSettings(RootInfo root);
 
     /**
      * Drops documents on a root.
      * @param check The check to make sure RootsFragment is not detached from activity.
      */
-    public boolean dropOn(ClipData data, RootInfo root) {
-        new GetRootDocumentTask(
-                root,
-                mActivity,
-                mActivity::isDestroyed,
-                (DocumentInfo doc) -> dropOn(data, root, doc)
-        ).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
-        return true;
-    }
+    boolean dropOn(ClipData data, RootInfo root);
 
-    private void dropOn(ClipData data, RootInfo root, DocumentInfo doc) {
-        DocumentClipper clipper
-                = DocumentsApplication.getDocumentClipper(mActivity);
-        clipper.copyFromClipData(root, doc, data, mActivity.fileOpCallback);
-    }
+    /**
+     * Attempts to eject the identified root. Returns a boolean answer to listener.
+     */
+    void ejectRoot(RootInfo root, BooleanConsumer listener);
 
-    public void ejectRoot(
-            RootInfo root, BooleanSupplier ejectCanceledCheck, Consumer<Boolean> listener) {
-        assert(ejectCanceledCheck != null);
-        ejectRoot(
-                root.authority,
-                root.rootId,
-                ejectCanceledCheck,
-                listener);
-    }
+    void showAppDetails(ResolveInfo info);
 
-    private void ejectRoot(
-            String authority,
-            String rootId,
-            BooleanSupplier ejectCanceledCheck,
-            Consumer<Boolean> listener) {
-        new EjectRootTask(
-                mActivity,
-                authority,
-                rootId,
-                ejectCanceledCheck,
-                listener).executeOnExecutor(ProviderExecutor.forAuthority(authority));
-    }
+    void openRoot(RootInfo root);
 
-    public void showAppDetails(ResolveInfo info) {
-        throw new UnsupportedOperationException("Can't show app details.");
-    }
+    void openRoot(ResolveInfo app);
 
-    public void openRoot(RootInfo root) {
-        Metrics.logRootVisited(mActivity, root);
-        mActivity.onRootPicked(root);
-    }
+    void openInNewWindow(DocumentStack path);
 
-    public void openRoot(ResolveInfo app) {
-        throw new UnsupportedOperationException("Can't open an app.");
-    }
+    void pasteIntoFolder(RootInfo root);
 
-    public void openInNewWindow(RootInfo root) {
-        throw new UnsupportedOperationException("Can't open in new window");
-    }
+    boolean viewDocument(DocumentDetails doc);
 
-    public void pasteIntoFolder(RootInfo root) {
-        throw new UnsupportedOperationException("Can't paste into folder.");
-    }
+    boolean previewDocument(DocumentDetails doc);
 
-    public boolean viewDocument(DocumentDetails doc) {
-        throw new UnsupportedOperationException("Direct view not supported!");
-    }
-
-    public boolean previewDocument(DocumentDetails doc) {
-        throw new UnsupportedOperationException("Preview not supported!");
-    }
-
-    public abstract boolean openDocument(DocumentDetails doc);
+    boolean openDocument(DocumentDetails doc);
 }
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index f7032aa..21570e4 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -158,10 +158,9 @@
      * Provides Activity a means of injection into and specialization of
      * fragment actions.
      *
-     * Args can be nullable when called from a contact without this information such as
-     * RootsFragment.
+     * Args can be nullable when called from a context lacking them, such as RootsFragment.
      */
-    public abstract ActionHandler<? extends BaseActivity> getActionHandler(
+    public abstract ActionHandler getActionHandler(
             @Nullable Model model, @Nullable MultiSelectManager selectionMgr);
 
     public abstract void onDocumentPicked(DocumentInfo doc, Model model);
diff --git a/src/com/android/documentsui/base/BooleanConsumer.java b/src/com/android/documentsui/base/BooleanConsumer.java
new file mode 100644
index 0000000..3b4dca8
--- /dev/null
+++ b/src/com/android/documentsui/base/BooleanConsumer.java
@@ -0,0 +1,22 @@
+/*
+ * 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.base;
+
+@FunctionalInterface
+public interface BooleanConsumer {
+
+    void accept(boolean value);
+}
diff --git a/src/com/android/documentsui/base/CheckedTask.java b/src/com/android/documentsui/base/CheckedTask.java
index 940763c..2dd6a2a 100644
--- a/src/com/android/documentsui/base/CheckedTask.java
+++ b/src/com/android/documentsui/base/CheckedTask.java
@@ -50,7 +50,7 @@
     protected abstract void finish(Output output);
 
     @Override
-    final protected void onPreExecute() {
+    protected final void onPreExecute() {
         if (mCheck.stop()) {
             return;
         }
@@ -58,7 +58,7 @@
     }
 
     @Override
-    final protected Output doInBackground(Input... input) {
+    protected final Output doInBackground(Input... input) {
         if (mCheck.stop()) {
             return null;
         }
@@ -66,7 +66,7 @@
     }
 
     @Override
-    final protected void onPostExecute(Output result) {
+    protected final void onPostExecute(Output result) {
         if (mCheck.stop()) {
             return;
         }
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 4684317..6b86e5f 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -79,6 +79,7 @@
 import com.android.documentsui.Snackbars;
 import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Events.InputEvent;
@@ -147,7 +148,7 @@
     private FocusManager mFocusManager;
 
     // This dependency is informally "injected" from the owning Activity in our onCreate method.
-    private ActionHandler<?> mActionHandler;
+    private ActionHandler mActionHandler;
 
     // This dependency is informally "injected" from the owning Activity in our onCreate method.
     private MenuManager mMenuManager;
@@ -671,11 +672,14 @@
         mTuner.showChooserForDoc(doc);
     }
 
+    // TODO: Once selection manager is activity owned, move this logic into
+    // a new method: ActionHandler#openWindowOnSelectedDocument
     private void openInNewWindow(final Selection selected) {
         assert(selected.size() == 1);
         DocumentInfo doc =
                 DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next()));
-        mTuner.openInNewWindow(getDisplayState().stack, doc);
+        assert(doc != null);
+        mActionHandler.openInNewWindow(new DocumentStack(getDisplayState().stack, doc));
     }
 
     private void shareDocuments(final Selection selected) {
diff --git a/src/com/android/documentsui/dirlist/FragmentTuner.java b/src/com/android/documentsui/dirlist/FragmentTuner.java
index 8d77f6e..2451bd2 100644
--- a/src/com/android/documentsui/dirlist/FragmentTuner.java
+++ b/src/com/android/documentsui/dirlist/FragmentTuner.java
@@ -17,7 +17,6 @@
 package com.android.documentsui.dirlist;
 
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.DocumentStack;
 
 /**
  * Providers support for specializing the DirectoryFragment to the "host" Activity.
@@ -56,10 +55,4 @@
     protected void showChooserForDoc(DocumentInfo doc) {
         throw new UnsupportedOperationException("Show chooser not supported!");
     }
-
-    // TODO: Move to action handler.
-    @Deprecated
-    protected void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
-        throw new UnsupportedOperationException("Open in new window not supported!");
-    }
 }
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
index 71308e4..36d0330 100644
--- a/src/com/android/documentsui/dirlist/UserInputHandler.java
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -44,7 +44,7 @@
 
     private static final String TAG = "UserInputHandler";
 
-    private ActionHandler<?> mActionHandler;
+    private ActionHandler mActionHandler;
     private final FocusHandler mFocusHandler;
     private final MultiSelectManager mSelectionMgr;
     private final Function<MotionEvent, T> mEventConverter;
@@ -61,7 +61,7 @@
     private final KeyInputHandler mKeyListener;
 
     public UserInputHandler(
-            ActionHandler<?> actionHandler,
+            ActionHandler actionHandler,
             FocusHandler focusHandler,
             MultiSelectManager selectionMgr,
             Function<MotionEvent, T> eventConverter,
diff --git a/src/com/android/documentsui/manager/ActionHandler.java b/src/com/android/documentsui/manager/ActionHandler.java
index 710bc48..c6022f2 100644
--- a/src/com/android/documentsui/manager/ActionHandler.java
+++ b/src/com/android/documentsui/manager/ActionHandler.java
@@ -16,10 +16,15 @@
 
 package com.android.documentsui.manager;
 
+import android.content.ClipData;
+import android.content.Intent;
+import android.provider.DocumentsContract;
 import android.util.Log;
 
+import com.android.documentsui.AbstractActionHandler;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.GetRootDocumentTask;
+import com.android.documentsui.Metrics;
 import com.android.documentsui.ProviderExecutor;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -35,36 +40,40 @@
 /**
  * Provides {@link ManageActivity} action specializations to fragments.
  */
-public class ActionHandler extends com.android.documentsui.ActionHandler<ManageActivity> {
+public class ActionHandler extends AbstractActionHandler<ManageActivity> {
 
     private static final String TAG = "ManagerActionHandler";
 
     private final FragmentTuner mTuner;
+    private final DocumentClipper mClipper;
     private final Config mConfig;
 
-    ActionHandler(ManageActivity activity, FragmentTuner tuner) {
+
+    ActionHandler(ManageActivity activity, FragmentTuner tuner, DocumentClipper clipper) {
         super(activity);
         mTuner = tuner;
+        mClipper = clipper;
         mConfig = new Config();
     }
 
     @Override
-    public void openSettings(RootInfo root) {
-        mActivity.openRootSettings(root);
-    }
-
-    @Override
-    public void openInNewWindow(RootInfo root) {
+    public boolean dropOn(ClipData data, RootInfo root) {
         new GetRootDocumentTask(
                 root,
                 mActivity,
                 mActivity::isDestroyed,
-                (DocumentInfo doc) -> openInNewWindow(root, doc)
+                (DocumentInfo doc) -> mClipper.copyFromClipData(
+                        root, doc, data, mActivity.fileOpCallback)
         ).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
+        return true;
     }
 
-    private void openInNewWindow(RootInfo root, DocumentInfo doc) {
-        mActivity.openInNewWindow(new DocumentStack(root), doc);
+    @Override
+    public void openSettings(RootInfo root) {
+        Metrics.logUserAction(mActivity, Metrics.USER_ACTION_SETTINGS);
+        final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
+        intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
+        mActivity.startActivity(intent);
     }
 
     @Override
@@ -84,6 +93,12 @@
     }
 
     @Override
+    public void openRoot(RootInfo root) {
+        Metrics.logRootVisited(mActivity, root);
+        mActivity.onRootPicked(root);
+    }
+
+    @Override
     public boolean openDocument(DocumentDetails details) {
         DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
         if (doc == null) {
diff --git a/src/com/android/documentsui/manager/LauncherActivity.java b/src/com/android/documentsui/manager/LauncherActivity.java
index 542c2c3..8bf3c5c 100644
--- a/src/com/android/documentsui/manager/LauncherActivity.java
+++ b/src/com/android/documentsui/manager/LauncherActivity.java
@@ -98,7 +98,7 @@
         startActivity(intent);
     }
 
-    static final Intent createLaunchIntent(Activity activity) {
+    public static final Intent createLaunchIntent(Activity activity) {
         Intent intent = new Intent(activity, ManageActivity.class);
         intent.setData(buildLaunchUri());
 
diff --git a/src/com/android/documentsui/manager/ManageActivity.java b/src/com/android/documentsui/manager/ManageActivity.java
index 332a3b0..ff82869 100644
--- a/src/com/android/documentsui/manager/ManageActivity.java
+++ b/src/com/android/documentsui/manager/ManageActivity.java
@@ -19,7 +19,6 @@
 import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
 import static com.android.documentsui.base.Shared.DEBUG;
 
-import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.FragmentManager;
 import android.content.ActivityNotFoundException;
@@ -27,7 +26,6 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.support.design.widget.Snackbar;
 import android.support.v7.widget.RecyclerView;
@@ -40,7 +38,6 @@
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
-import com.android.documentsui.Metrics;
 import com.android.documentsui.OperationDialogFragment;
 import com.android.documentsui.OperationDialogFragment.DialogType;
 import com.android.documentsui.ProviderExecutor;
@@ -102,7 +99,7 @@
         mTuner = new Tuner(this, mState);
         // Make sure this is done after the RecyclerView and the Model are set up.
         mFocusManager = new FocusManager(getColor(R.color.accent_dark));
-        mActionHandler = new ActionHandler(this, mTuner);
+        mActionHandler = new ActionHandler(this, mTuner, mClipper);
         mClipper = DocumentsApplication.getDocumentClipper(this);
 
         RootsFragment.show(getFragmentManager(), null);
@@ -112,7 +109,7 @@
 
         if (mState.restored) {
             if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
-        } else if (!mState.stack.isEmpty()) {
+        } else if (mState.stack.root != null) {
             // If a non-empty stack is present in our state, it was read (presumably)
             // from EXTRA_STACK intent extra. In this case, we'll skip other means of
             // loading or restoring the stack (like URI).
@@ -124,16 +121,22 @@
             //
             // Any other URI is *sorta* unexpected...except when browsing an archive
             // in downloads.
-            if(uri != null
-                    && uri.getAuthority() != null
-                    && !uri.equals(mState.stack.peek())
-                    && !LauncherActivity.isLaunchUri(uri)) {
-                if (DEBUG) Log.w(TAG,
-                        "Launching with non-empty stack. Ignoring unexpected uri: " + uri);
-            } else {
-                if (DEBUG) Log.d(TAG, "Launching with non-empty stack.");
+            if (DEBUG) {
+                if (uri != null
+                        && uri.getAuthority() != null
+                        && !uri.equals(mState.stack.peek())
+                        && !LauncherActivity.isLaunchUri(uri)) {
+                    Log.w(TAG, "Launching with non-empty stack. Ignoring unexpected uri: " + uri);
+                } else {
+                    Log.d(TAG, "Launching with non-empty stack.");
+                }
             }
-            refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+
+            if (!mState.stack.isEmpty()) {
+                refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+            } else {
+                onRootPicked(mState.stack.root);
+            }
         } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             assert(uri != null);
             new OpenUriForViewTask(this).executeOnExecutor(
@@ -237,7 +240,7 @@
                 showCreateDirectoryDialog();
                 break;
             case R.id.menu_new_window:
-                openInNewWindow(mState.stack, null);
+                mActionHandler.openInNewWindow(mState.stack);
                 break;
             case R.id.menu_paste_from_clipboard:
                 DirectoryFragment dir = getDirectoryFragment();
@@ -246,8 +249,7 @@
                 }
                 break;
             case R.id.menu_settings:
-                final RootInfo root = getCurrentRoot();
-                openRootSettings(root);
+                mActionHandler.openSettings(getCurrentRoot());
                 break;
             default:
                 return super.onOptionsItemSelected(item);
@@ -255,35 +257,6 @@
         return true;
     }
 
-    void openRootSettings(RootInfo root) {
-        Metrics.logUserAction(this, Metrics.USER_ACTION_SETTINGS);
-        final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
-        intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
-        startActivity(intent);
-    }
-
-    /**
-     * Opens a new window at given location. If doc is null then it opens the stack. If doc is not
-     * null it pushes the doc to the stack and opens it.
-     */
-    public void openInNewWindow(DocumentStack stack, @Nullable DocumentInfo doc) {
-        Metrics.logUserAction(this, Metrics.USER_ACTION_NEW_WINDOW);
-
-        Intent intent = LauncherActivity.createLaunchIntent(this);
-
-        stack = (doc == null) ? stack : new DocumentStack(stack, doc);
-        intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
-
-        // With new multi-window mode we have to pick how we are launched.
-        // By default we'd be launched in-place above the existing app.
-        // By setting launch-to-side ActivityManager will open us to side.
-        if (isInMultiWindowMode()) {
-            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
-        }
-
-        startActivity(intent);
-    }
-
     @Override
     public void refreshDirectory(int anim) {
         final FragmentManager fm = getFragmentManager();
diff --git a/src/com/android/documentsui/manager/Tuner.java b/src/com/android/documentsui/manager/Tuner.java
index abf8abb..f783a42 100644
--- a/src/com/android/documentsui/manager/Tuner.java
+++ b/src/com/android/documentsui/manager/Tuner.java
@@ -17,7 +17,6 @@
 package com.android.documentsui.manager;
 
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.FragmentTuner;
@@ -31,8 +30,6 @@
  */
 public final class Tuner extends FragmentTuner {
 
-    private static final String TAG = "ManageTuner";
-
     private final ManageActivity mActivity;
     private final State mState;
 
@@ -83,12 +80,6 @@
         mActivity.showChooserForDoc(doc);
     }
 
-    // TODO: Move to action handler.
-    @Override
-    public void openInNewWindow(DocumentStack stack, DocumentInfo doc) {
-        mActivity.openInNewWindow(stack, doc);
-    }
-
     Tuner reset(Model model, boolean searchMode) {
         mConfig.reset(model, searchMode);
         return this;
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 0353169..e661ac5 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -22,8 +22,11 @@
 import android.provider.Settings;
 import android.util.Log;
 
+import com.android.documentsui.AbstractActionHandler;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.dirlist.FragmentTuner;
 import com.android.documentsui.dirlist.Model;
@@ -34,7 +37,7 @@
 /**
  * Provides {@link PickActivity} action specializations to fragments.
  */
-class ActionHandler extends com.android.documentsui.ActionHandler<PickActivity> {
+class ActionHandler extends AbstractActionHandler<PickActivity> {
 
     private static final String TAG = "PickerActionHandler";
 
@@ -56,6 +59,20 @@
     }
 
     @Override
+    public void openInNewWindow(DocumentStack path) {
+        // Open new window support only depends on vanilla Activity, so it is
+        // implemented in our parent class. But we don't support that in
+        // picking. So as a matter of defensiveness, we override that here.
+        throw new UnsupportedOperationException("Can't open in new window");
+    }
+
+    @Override
+    public void openRoot(RootInfo root) {
+        Metrics.logRootVisited(mActivity, root);
+        mActivity.onRootPicked(root);
+    }
+
+    @Override
     public void openRoot(ResolveInfo info) {
         Metrics.logAppVisited(mActivity, info);
         mActivity.onAppPicked(info);
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 112af76..9756c83 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -81,8 +81,8 @@
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
+
         mTuner = new Tuner(this, mState);
-        // Make sure this is done after the RecyclerView and the Model are set up.
         mFocusManager = new FocusManager(getColor(R.color.accent_dark));
         mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
         mActionHandler = new ActionHandler(this, mTuner);
diff --git a/src/com/android/documentsui/picker/Tuner.java b/src/com/android/documentsui/picker/Tuner.java
index 4804d75..cccb01f 100644
--- a/src/com/android/documentsui/picker/Tuner.java
+++ b/src/com/android/documentsui/picker/Tuner.java
@@ -30,7 +30,6 @@
 import com.android.documentsui.dirlist.FragmentTuner;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
-import com.android.documentsui.dirlist.MultiSelectManager;
 
 import javax.annotation.Nullable;
 
@@ -135,7 +134,6 @@
     private static final class Config {
 
         @Nullable Model model;
-        @Nullable MultiSelectManager selectionMgr;
         boolean searchMode;
 
         private final EventListener<Update> mModelUpdateListener;
@@ -149,12 +147,10 @@
         private boolean modelLoadObserved;
 
         public void reset(Model model, boolean searchMode) {
-            this.searchMode = searchMode;
             assert(model != null);
-            assert(selectionMgr != null);
 
+            this.searchMode = searchMode;
             this.model = model;
-            this.selectionMgr = selectionMgr;
 
             model.addUpdateListener(mModelUpdateListener);
             modelLoadObserved = false;
diff --git a/src/com/android/documentsui/sidebar/AppItem.java b/src/com/android/documentsui/sidebar/AppItem.java
index 4af4022..d13b475 100644
--- a/src/com/android/documentsui/sidebar/AppItem.java
+++ b/src/com/android/documentsui/sidebar/AppItem.java
@@ -24,8 +24,8 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.android.documentsui.R;
 import com.android.documentsui.ActionHandler;
+import com.android.documentsui.R;
 
 /**
  * An {@link Item} for apps that supports some picking actions like
@@ -36,9 +36,9 @@
 
     public final ResolveInfo info;
 
-    private final ActionHandler<?> mActionHandler;
+    private final ActionHandler mActionHandler;
 
-    public AppItem(ResolveInfo info, ActionHandler<?> actionHandler) {
+    public AppItem(ResolveInfo info, ActionHandler actionHandler) {
         super(R.layout.item_root, getStringId(info));
         this.info = info;
 
diff --git a/src/com/android/documentsui/sidebar/EjectRootTask.java b/src/com/android/documentsui/sidebar/EjectRootTask.java
index 1f486ff..2ce0658 100644
--- a/src/com/android/documentsui/sidebar/EjectRootTask.java
+++ b/src/com/android/documentsui/sidebar/EjectRootTask.java
@@ -18,50 +18,45 @@
 
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
-import android.content.Context;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.provider.DocumentsContract;
 import android.util.Log;
 
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.base.CheckedTask;
+import com.android.documentsui.base.BooleanConsumer;
 import com.android.documentsui.base.Shared;
 
-import java.util.function.BooleanSupplier;
-import java.util.function.Consumer;
+public final class EjectRootTask extends AsyncTask<Void, Void, Boolean> {
 
-public final class EjectRootTask extends CheckedTask<Void, Boolean> {
+    private final ContentResolver mResolver;
     private final String mAuthority;
     private final String mRootId;
-    private final Consumer<Boolean> mCallback;
-    private Context mContext;
+    private final BooleanConsumer mCallback;
 
     /**
      * @param ejectCanceledCheck The method reference we use to see whether eject should be stopped
      * at any point
      * @param finishCallback The end callback necessary when the eject task finishes
      */
-    public EjectRootTask(Context context,
+    public EjectRootTask(
+            ContentResolver resolver,
             String authority,
             String rootId,
-            BooleanSupplier ejectCanceledCheck,
-            Consumer<Boolean> finishCallback) {
-        super(ejectCanceledCheck::getAsBoolean);
+            BooleanConsumer finishCallback) {
+        mResolver = resolver;
         mAuthority = authority;
         mRootId = rootId;
-        mContext = context;
         mCallback = finishCallback;
     }
 
     @Override
-    protected Boolean run(Void... params) {
-        final ContentResolver resolver = mContext.getContentResolver();
-
+    protected Boolean doInBackground(Void... args) {
         Uri rootUri = DocumentsContract.buildRootUri(mAuthority, mRootId);
         ContentProviderClient client = null;
         try {
             client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                    resolver, mAuthority);
+                    mResolver, mAuthority);
             return DocumentsContract.ejectRoot(client, rootUri);
         } catch (Exception e) {
             Log.w(Shared.TAG, "Failed to eject root", e);
@@ -73,7 +68,7 @@
     }
 
     @Override
-    protected void finish(Boolean ejected) {
+    protected void onPostExecute(Boolean ejected) {
         mCallback.accept(ejected);
     }
 }
\ No newline at end of file
diff --git a/src/com/android/documentsui/sidebar/RootItem.java b/src/com/android/documentsui/sidebar/RootItem.java
index 30984ec..4410b46 100644
--- a/src/com/android/documentsui/sidebar/RootItem.java
+++ b/src/com/android/documentsui/sidebar/RootItem.java
@@ -31,7 +31,6 @@
 import com.android.documentsui.ActionHandler;
 import com.android.documentsui.MenuManager;
 import com.android.documentsui.R;
-import com.android.documentsui.base.CheckedTask.Check;
 import com.android.documentsui.base.RootInfo;
 
 /**
@@ -42,9 +41,9 @@
 
     public final RootInfo root;
 
-    private final ActionHandler<?> mActionHandler;
+    private final ActionHandler mActionHandler;
 
-    public RootItem(RootInfo root, ActionHandler<?> actionHandler) {
+    public RootItem(RootInfo root, ActionHandler actionHandler) {
         super(R.layout.item_root, getStringId(root));
         this.root = root;
         mActionHandler = actionHandler;
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 9e81ed0..1cbed8e 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -51,6 +51,8 @@
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.ItemDragListener;
 import com.android.documentsui.R;
+import com.android.documentsui.base.BooleanConsumer;
+import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
@@ -64,7 +66,6 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
-import java.util.function.Consumer;
 
 /**
  * Display list of known storage backend roots.
@@ -107,7 +108,7 @@
     private ListView mList;
     private RootsAdapter mAdapter;
     private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
-    private ActionHandler<?> mActionHandler;
+    private ActionHandler mActionHandler;
 
     public static RootsFragment show(FragmentManager fm, Intent includeApps) {
         final Bundle args = new Bundle();
@@ -374,7 +375,7 @@
                 ejectClicked(ejectIcon, rootItem.root, mActionHandler);
                 return true;
             case R.id.menu_open_in_new_window:
-                mActionHandler.openInNewWindow(rootItem.root);
+                mActionHandler.openInNewWindow(new DocumentStack(rootItem.root));
                 return true;
             case R.id.menu_paste_into_folder:
                 mActionHandler.pasteIntoFolder(rootItem.root);
@@ -388,19 +389,24 @@
         }
     }
 
-    static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler<?> actionHandler) {
+    static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
         assert(ejectIcon != null);
         assert(!root.ejecting);
         ejectIcon.setEnabled(false);
         root.ejecting = true;
         actionHandler.ejectRoot(
                 root,
-                () -> ejectIcon.getVisibility() != View.VISIBLE,
-                new Consumer<Boolean>() {
+                new BooleanConsumer() {
                     @Override
-                    public void accept(Boolean ejected) {
-                        ejectIcon.setEnabled(!ejected);
+                    public void accept(boolean ejected) {
+                        // Event if ejected is false, we should reset, since the op failed.
+                        // Either way, we are no longer attempting to eject the device.
                         root.ejecting = false;
+
+                        // If the view is still visible, we update its state.
+                        if (ejectIcon.getVisibility() == View.VISIBLE) {
+                            ejectIcon.setEnabled(!ejected);
+                        }
                     }
                 });
     }
diff --git a/tests/common/com/android/documentsui/testing/Roots.java b/tests/common/com/android/documentsui/testing/Roots.java
new file mode 100644
index 0000000..576301a
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/Roots.java
@@ -0,0 +1,30 @@
+/*
+ * 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.testing;
+
+import com.android.documentsui.base.RootInfo;
+
+public final class Roots {
+
+    private Roots() {}
+
+    public static RootInfo create(String id) {
+        RootInfo root = new RootInfo();
+        root.authority = "test-authority";
+        root.rootId = id;
+        return root;
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 6914127..c617a43 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -16,11 +16,11 @@
 
 package com.android.documentsui.testing;
 
-import com.android.documentsui.ActionHandler;
-import com.android.documentsui.BaseActivity;
+import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.dirlist.DocumentDetails;
 
-public class TestActionHandler extends ActionHandler<BaseActivity> {
+public class TestActionHandler extends AbstractActionHandler {
 
     public final TestEventHandler<DocumentDetails> open = new TestEventHandler<>();
     public final TestEventHandler<DocumentDetails> view = new TestEventHandler<>();
@@ -44,4 +44,9 @@
     public boolean previewDocument(DocumentDetails doc) {
         return preview.accept(doc);
     }
+
+    @Override
+    public void openRoot(RootInfo root) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/tests/common/com/android/documentsui/testing/TestActivity.java b/tests/common/com/android/documentsui/testing/TestActivity.java
new file mode 100644
index 0000000..8cdc7d5
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestActivity.java
@@ -0,0 +1,49 @@
+/*
+ * 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.testing;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import junit.framework.Assert;
+
+import org.mockito.Mockito;
+
+import javax.annotation.Nullable;
+
+public abstract class TestActivity extends Activity {
+
+    private @Nullable Intent mLastStarted;
+
+    public static TestActivity create() {
+        return Mockito.mock(TestActivity.class);
+    }
+
+    @Override
+    public String getPackageName() {
+        return "TestActivity";
+    }
+
+    @Override
+    public void startActivity(Intent intent) {
+        mLastStarted = intent;
+    }
+
+    public void assertStarted(Intent expected) {
+        Assert.assertEquals(expected, mLastStarted);
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestConsumer.java b/tests/common/com/android/documentsui/testing/TestConsumer.java
new file mode 100644
index 0000000..ce89292
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestConsumer.java
@@ -0,0 +1,33 @@
+/*
+ * 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.testing;
+
+import com.android.documentsui.base.EventHandler;
+
+import java.util.function.Consumer;
+
+/**
+ * Test {@link EventHandler} that can be used to spy on,  control responses from,
+ * and make assertions against values tested.
+ */
+public class TestConsumer<T> extends TestPredicate<T> implements Consumer<T> {
+
+    @Override
+    public void accept(T event) {
+        test(event);
+    }
+}
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
new file mode 100644
index 0000000..c6c66d0
--- /dev/null
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.content.Intent;
+import android.os.Parcelable;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.dirlist.DocumentDetails;
+import com.android.documentsui.manager.LauncherActivity;
+import com.android.documentsui.testing.Roots;
+import com.android.documentsui.testing.TestActivity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AbstractActionHandlerTest {
+
+    private TestActivity mActivity;
+    private AbstractActionHandler<TestActivity> mHandler;
+
+    @Before
+    public void setUp() {
+        mActivity = TestActivity.create();
+        mHandler = new AbstractActionHandler<TestActivity>(mActivity) {
+
+            @Override
+            public void openRoot(RootInfo root) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public boolean openDocument(DocumentDetails doc) {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    @Test
+    public void testOpenNewWindow() {
+        DocumentStack path = new DocumentStack(Roots.create("123"));
+        mHandler.openInNewWindow(path);
+
+        Intent expected = LauncherActivity.createLaunchIntent(mActivity);
+        expected.putExtra(Shared.EXTRA_STACK, (Parcelable) path);
+        mActivity.assertStarted(expected);
+    }
+}