Inject SelectionManager instead of passing on reset.

Move more delete functionality into ActionHandler.
Make ActionModeController Activity scoped.
Name most "Config" objects (relating to scope) to ContentScope.
Clear focus info when resetting FocusManager.

Bug: 31658228

Change-Id: I6dbdb07ded83c75b82aec5773a60ef0febb6f02c
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 9eb1359..d626b70 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -40,6 +40,7 @@
 import com.android.documentsui.roots.LoadRootTask;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.selection.Selection;
+import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.sidebar.EjectRootTask;
 
 import java.util.List;
@@ -55,6 +56,7 @@
     protected final State mState;
     protected final RootsAccess mRoots;
     protected final DocumentsAccess mDocs;
+    protected final SelectionManager mSelectionMgr;
     protected final Lookup<String, Executor> mExecutors;
 
     public AbstractActionHandler(
@@ -62,17 +64,20 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
+            SelectionManager selectionMgr,
             Lookup<String, Executor> executors) {
 
         assert(activity != null);
         assert(state != null);
         assert(roots != null);
+        assert(selectionMgr != null);
         assert(docs != null);
 
         mActivity = activity;
         mState = state;
         mRoots = roots;
         mDocs = docs;
+        mSelectionMgr = selectionMgr;
         mExecutors = executors;
     }
 
@@ -86,6 +91,11 @@
     }
 
     @Override
+    public void openSelectedInNewWindow() {
+        throw new UnsupportedOperationException("Can't open in new window.");
+    }
+
+    @Override
     public void openInNewWindow(DocumentStack path) {
         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_NEW_WINDOW);
 
@@ -143,7 +153,7 @@
     }
 
     @Override
-    public void deleteDocuments(Model model, Selection selection, ConfirmationCallback callback) {
+    public void deleteSelectedDocuments(Model model, ConfirmationCallback callback) {
         throw new UnsupportedOperationException("Delete not supported!");
     }
 
@@ -163,6 +173,9 @@
         loadRoot(Shared.getDefaultRootUri(mActivity));
     }
 
+    protected Selection getStableSelection() {
+        return mSelectionMgr.getSelection(new Selection());
+    }
     /**
      * A class primarily for the support of isolating our tests
      * from our concrete activity implementations.
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 5ee3977..15ee5bc 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -28,7 +28,6 @@
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.selection.Selection;
 
 public interface ActionHandler {
 
@@ -54,6 +53,8 @@
 
     void loadDocument(Uri uri);
 
+    void openSelectedInNewWindow();
+
     void openInNewWindow(DocumentStack path);
 
     void pasteIntoFolder(RootInfo root);
@@ -66,7 +67,7 @@
 
     void showChooserForDoc(DocumentInfo doc);
 
-    void deleteDocuments(Model model, Selection selection, ConfirmationCallback callback);
+    void deleteSelectedDocuments(Model model, ConfirmationCallback callback);
 
     /**
      * Called when initial activity setup is complete. Implementations
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 997a9bd..0f285f5 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -52,9 +52,11 @@
 import android.view.View;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
+import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.NavigationViewManager.Breadcrumb;
 import com.android.documentsui.SearchViewManager.SearchManagerListener;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.LocalPreferences;
 import com.android.documentsui.base.PairedTask;
@@ -62,19 +64,21 @@
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 import com.android.documentsui.base.State.ViewMode;
+import com.android.documentsui.dirlist.ActionModeController;
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsCache;
+import com.android.documentsui.selection.Selection;
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
-import com.android.documentsui.selection.Selection;
 import com.android.documentsui.sidebar.RootsFragment;
 import com.android.documentsui.sorting.SortController;
 import com.android.documentsui.sorting.SortModel;
 import com.android.documentsui.ui.DialogController;
+import com.android.documentsui.ui.MessageBuilder;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -92,11 +96,13 @@
 
     protected @Nullable RetainedState mRetainedState;
     protected RootsCache mRoots;
+    protected MessageBuilder mMessages;
     protected DrawerController mDrawer;
     protected NavigationViewManager mNavigator;
-    List<EventListener> mEventListeners = new ArrayList<>();
+    protected FocusManager mFocusManager;
     protected SortController mSortController;
 
+    private final List<EventListener> mEventListeners = new ArrayList<>();
     private final String mTag;
     private final ContentObserver mRootsCacheObserver = new ContentObserver(
             new Handler()) {
@@ -133,12 +139,6 @@
 
     /**
      * Provides Activity a means of injection into and specialization of
-     * DirectoryFragment.
-     */
-    public abstract FocusManager getFocusManager(RecyclerView view, Model model);
-
-    /**
-     * Provides Activity a means of injection into and specialization of
      * DirectoryFragment hosted menus.
      */
     public abstract MenuManager getMenuManager();
@@ -155,8 +155,24 @@
      *
      * Args can be null when called from a context lacking fragment, such as RootsFragment.
      */
-    public abstract ActionHandler getActionHandler(
-            @Nullable Model model, @Nullable SelectionManager selectionMgr, boolean searchMode);
+    public abstract ActionHandler getActionHandler(@Nullable Model model, boolean searchMode);
+
+    /**
+     * Provides Activity a means of injection into and specialization of
+     * DirectoryFragment.
+     */
+    public abstract ActionModeController getActionModeController(
+            SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view);
+
+    public final FocusManager getFocusManager(RecyclerView view, Model model) {
+        assert(mFocusManager != null);
+        return mFocusManager.reset(view, model);
+    }
+
+    public final MessageBuilder getMessages() {
+        assert(mMessages != null);
+        return mMessages;
+    }
 
     public BaseActivity(@LayoutRes int layoutId, String tag) {
         mLayoutId = layoutId;
@@ -178,6 +194,7 @@
         setContentView(mLayoutId);
 
         mState = getState(icicle);
+        mFocusManager = new FocusManager(getColor(R.color.accent_dark));
         mDrawer = DrawerController.create(this, getActivityConfig());
         Metrics.logActivityLaunch(this, mState, intent);
 
@@ -186,7 +203,7 @@
         // support to that fragment.
         mRetainedState = (RetainedState) getLastNonConfigurationInstance();
         mRoots = DocumentsApplication.getRootsCache(this);
-
+        mMessages = new MessageBuilder(this);
         getContentResolver().registerContentObserver(
                 RootsCache.sNotificationUri, false, mRootsCacheObserver);
 
diff --git a/src/com/android/documentsui/FocusManager.java b/src/com/android/documentsui/FocusManager.java
index 39f2e5c..2371b7f 100644
--- a/src/com/android/documentsui/FocusManager.java
+++ b/src/com/android/documentsui/FocusManager.java
@@ -57,11 +57,8 @@
 public final class FocusManager implements FocusHandler {
     private static final String TAG = "FocusManager";
 
-    private final Config mConfig = new Config();
-    @Nullable TitleSearchHelper mSearchHelper;
-    private @Nullable String mPendingFocusId;
-
-    private int mLastFocusPosition = RecyclerView.NO_POSITION;
+    private final ContentScope mScope = new ContentScope();
+    private final TitleSearchHelper mSearchHelper;
 
     public FocusManager(@ColorRes int color) {
         mSearchHelper = new TitleSearchHelper(color);
@@ -91,24 +88,24 @@
     @Override
     public void onFocusChange(View v, boolean hasFocus) {
         // Remember focus events on items.
-        if (hasFocus && v.getParent() == mConfig.mView) {
-            mLastFocusPosition = mConfig.mView.getChildAdapterPosition(v);
+        if (hasFocus && v.getParent() == mScope.view) {
+            mScope.lastFocusPosition = mScope.view.getChildAdapterPosition(v);
         }
     }
 
     @Override
     public void restoreLastFocus() {
-        if (mConfig.mAdapter.getItemCount() == 0) {
+        if (mScope.adapter.getItemCount() == 0) {
             // Nothing to focus.
             return;
         }
 
-        if (mLastFocusPosition != RecyclerView.NO_POSITION) {
+        if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) {
             // The system takes care of situations when a view is no longer on screen, etc,
-            focusItem(mLastFocusPosition);
+            focusItem(mScope.lastFocusPosition);
         } else {
             // Focus the first visible item
-            focusItem(mConfig.mLayout.findFirstVisibleItemPosition());
+            focusItem(mScope.layout.findFirstVisibleItemPosition());
         }
     }
 
@@ -118,15 +115,15 @@
      */
     @Override
     public void onLayoutCompleted() {
-        if (mPendingFocusId == null) {
+        if (mScope.pendingFocusId == null) {
             return;
         }
 
-        int pos = mConfig.mAdapter.getModelIds().indexOf(mPendingFocusId);
+        int pos = mScope.adapter.getModelIds().indexOf(mScope.pendingFocusId);
         if (pos != -1) {
             focusItem(pos);
         }
-        mPendingFocusId = null;
+        mScope.pendingFocusId = null;
     }
 
     /*
@@ -136,17 +133,17 @@
      */
     @Override
     public void focusDocument(String modelId) {
-        int pos = mConfig.mAdapter.getModelIds().indexOf(modelId);
-        if (pos != -1 && mConfig.mView.findViewHolderForAdapterPosition(pos) != null) {
+        int pos = mScope.adapter.getModelIds().indexOf(modelId);
+        if (pos != -1 && mScope.view.findViewHolderForAdapterPosition(pos) != null) {
             focusItem(pos);
         } else {
-            mPendingFocusId = modelId;
+            mScope.pendingFocusId = modelId;
         }
     }
 
     @Override
     public int getFocusPosition() {
-        return mLastFocusPosition;
+        return mScope.lastFocusPosition;
     }
 
     /**
@@ -162,7 +159,7 @@
             case KeyEvent.KEYCODE_MOVE_HOME:
                 return 0;
             case KeyEvent.KEYCODE_MOVE_END:
-                return mConfig.mAdapter.getItemCount() - 1;
+                return mScope.adapter.getItemCount() - 1;
             case KeyEvent.KEYCODE_PAGE_UP:
             case KeyEvent.KEYCODE_PAGE_DOWN:
                 return findPagedTargetPosition(view, keyCode, event);
@@ -180,7 +177,7 @@
         }
 
         if (inGridMode()) {
-            int currentPosition = mConfig.mView.getChildAdapterPosition(view);
+            int currentPosition = mScope.view.getChildAdapterPosition(view);
             // Left and right arrow keys only work in grid mode.
             switch (keyCode) {
                 case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -191,7 +188,7 @@
                     }
                     break;
                 case KeyEvent.KEYCODE_DPAD_RIGHT:
-                    if (currentPosition < mConfig.mAdapter.getItemCount() - 1) {
+                    if (currentPosition < mScope.adapter.getItemCount() - 1) {
                         // Stop forward focus search at the last item, otherwise focus will wrap
                         // around to the first visible item.
                         searchDir = View.FOCUS_FORWARD;
@@ -206,15 +203,15 @@
             // events that cause a UI rebuild (like rotating the device). Compromise: turn focusable
             // off while performing the focus search.
             // TODO: Revisit this when RV focus issues are resolved.
-            mConfig.mView.setFocusable(false);
+            mScope.view.setFocusable(false);
             View targetView = view.focusSearch(searchDir);
-            mConfig.mView.setFocusable(true);
+            mScope.view.setFocusable(true);
             // TargetView can be null, for example, if the user pressed <down> at the bottom
             // of the list.
             if (targetView != null) {
                 // Ignore navigation targets that aren't items in the RecyclerView.
-                if (targetView.getParent() == mConfig.mView) {
-                    return mConfig.mView.getChildAdapterPosition(targetView);
+                if (targetView.getParent() == mScope.view) {
+                    return mScope.view.getChildAdapterPosition(targetView);
                 }
             }
         }
@@ -236,9 +233,9 @@
      * @return The adapter position of the target item.
      */
     private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) {
-        int first = mConfig.mLayout.findFirstVisibleItemPosition();
-        int last = mConfig.mLayout.findLastVisibleItemPosition();
-        int current = mConfig.mView.getChildAdapterPosition(view);
+        int first = mScope.layout.findFirstVisibleItemPosition();
+        int last = mScope.layout.findLastVisibleItemPosition();
+        int current = mScope.view.getChildAdapterPosition(view);
         int pageSize = last - first + 1;
 
         if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
@@ -259,7 +256,7 @@
             } else {
                 // If the current item is the last item, target the item one page down.
                 int target = current + pageSize;
-                int max = mConfig.mAdapter.getItemCount() - 1;
+                int max = mScope.adapter.getItemCount() - 1;
                 return target < max ? target : max;
             }
         }
@@ -285,20 +282,20 @@
      * @param callback A callback to call after the given item has been focused.
      */
     private void focusItem(final int pos, @Nullable final FocusCallback callback) {
-        if (mPendingFocusId != null) {
-            Log.v(TAG, "clearing pending focus id: " + mPendingFocusId);
-            mPendingFocusId = null;
+        if (mScope.pendingFocusId != null) {
+            Log.v(TAG, "clearing pending focus id: " + mScope.pendingFocusId);
+            mScope.pendingFocusId = null;
         }
 
         // If the item is already in view, focus it; otherwise, scroll to it and focus it.
-        RecyclerView.ViewHolder vh = mConfig.mView.findViewHolderForAdapterPosition(pos);
+        RecyclerView.ViewHolder vh = mScope.view.findViewHolderForAdapterPosition(pos);
         if (vh != null) {
             if (vh.itemView.requestFocus() && callback != null) {
                 callback.onFocus(vh.itemView);
             }
         } else {
             // Set a one-time listener to request focus when the scroll has completed.
-            mConfig.mView.addOnScrollListener(
+            mScope.view.addOnScrollListener(
                     new RecyclerView.OnScrollListener() {
                         @Override
                         public void onScrollStateChanged(RecyclerView view, int newState) {
@@ -320,7 +317,7 @@
                             }
                         }
                     });
-            mConfig.mView.smoothScrollToPosition(pos);
+            mScope.view.smoothScrollToPosition(pos);
         }
     }
 
@@ -328,7 +325,7 @@
      * @return Whether the layout manager is currently in a grid-configuration.
      */
     private boolean inGridMode() {
-        return mConfig.mLayout.getSpanCount() > 1;
+        return mScope.layout.getSpanCount() > 1;
     }
 
     private interface FocusCallback {
@@ -428,7 +425,7 @@
         private void search() {
             if (!mActive) {
                 // The model listener invalidates the search index when the model changes.
-                mConfig.mModel.addUpdateListener(mModelListener);
+                mScope.model.addUpdateListener(mModelListener);
 
                 // Used to keep the current search alive until the timeout expires. If the user
                 // presses another key within that time, that keystroke is added to the current
@@ -470,7 +467,7 @@
          */
         private void endSearch() {
             if (mActive) {
-                mConfig.mModel.removeUpdateListener(mModelListener);
+                mScope.model.removeUpdateListener(mModelListener);
                 mTimer.cancel();
             }
 
@@ -486,11 +483,11 @@
          * must be set up before calling this method.
          */
         private void buildIndex() {
-            int itemCount = mConfig.mAdapter.getItemCount();
+            int itemCount = mScope.adapter.getItemCount();
             List<String> index = new ArrayList<>(itemCount);
             for (int i = 0; i < itemCount; i++) {
-                String modelId = mConfig.mAdapter.getModelId(i);
-                Cursor cursor = mConfig.mModel.getItem(modelId);
+                String modelId = mScope.adapter.getModelId(i);
+                Cursor cursor = mScope.model.getItem(modelId);
                 if (modelId != null && cursor != null) {
                     String title = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
                     // Perform case-insensitive search.
@@ -568,25 +565,26 @@
     }
 
     public FocusManager reset(RecyclerView view, Model model) {
-        mConfig.reset(view, model);
+        assert (view != null);
+        assert (model != null);
+        mScope.view = view;
+        mScope.adapter = (DocumentsAdapter) view.getAdapter();
+        mScope.layout = (GridLayoutManager) view.getLayoutManager();
+        mScope.model = model;
+
+        mScope.lastFocusPosition = RecyclerView.NO_POSITION;
+        mScope.pendingFocusId = null;
+
         return this;
     }
 
-    private static final class Config {
+    private static final class ContentScope {
+        private @Nullable RecyclerView view;
+        private @Nullable DocumentsAdapter adapter;
+        private @Nullable GridLayoutManager layout;
+        private @Nullable Model model;
 
-        @Nullable RecyclerView mView;
-        @Nullable DocumentsAdapter mAdapter;
-        @Nullable GridLayoutManager mLayout;
-
-        private Model mModel;
-
-        public void reset(RecyclerView view, Model model) {
-            assert (view != null);
-            assert (model != null);
-            mView = view;
-            mAdapter = (DocumentsAdapter) view.getAdapter();
-            mLayout = (GridLayoutManager) view.getLayoutManager();
-            mModel = model;
-        }
+        private @Nullable String pendingFocusId;
+        private int lastFocusPosition = RecyclerView.NO_POSITION;
     }
 }
diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java
index 154b397..cfa102d 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui.base;
 
+import android.annotation.PluralsRes;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
@@ -131,9 +132,10 @@
     }
 
     /**
-     * Generates a formatted quantity string.
+     * @deprecated use {@ link MessageBuilder#getQuantityString}
      */
-    public static final String getQuantityString(Context context, int resourceId, int quantity) {
+    @Deprecated
+    public static final String getQuantityString(Context context, @PluralsRes int resourceId, int quantity) {
         return context.getResources().getQuantityString(resourceId, quantity, quantity);
     }
 
diff --git a/src/com/android/documentsui/dirlist/ActionModeController.java b/src/com/android/documentsui/dirlist/ActionModeController.java
index 6952d47..03582f4 100644
--- a/src/com/android/documentsui/dirlist/ActionModeController.java
+++ b/src/com/android/documentsui/dirlist/ActionModeController.java
@@ -21,7 +21,6 @@
 import android.annotation.IdRes;
 import android.annotation.Nullable;
 import android.app.Activity;
-import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ActionMode;
@@ -31,17 +30,17 @@
 import android.view.View;
 
 import com.android.documentsui.MenuManager;
+import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.R;
 import com.android.documentsui.base.ConfirmationCallback;
 import com.android.documentsui.base.ConfirmationCallback.Result;
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Menus;
-import com.android.documentsui.base.Shared;
 import com.android.documentsui.selection.Selection;
 import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.ui.MessageBuilder;
 
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.IntConsumer;
 
 /**
@@ -51,42 +50,27 @@
 
     private static final String TAG = "ActionModeController";
 
-    private final Context mContext;
+    private final Activity mActivity;
     private final SelectionManager mSelectionMgr;
     private final MenuManager mMenuManager;
-    private final MenuManager.SelectionDetails mSelectionDetails;
+    private final MessageBuilder mMessages;
 
-    private final Function<ActionMode.Callback, ActionMode> mActionModeFactory;
-    private final EventHandler<MenuItem> mMenuItemClicker;
-    private final IntConsumer mHapticPerformer;
-    private final Consumer<CharSequence> mAccessibilityAnnouncer;
-    private final AccessibilityImportanceSetter mAccessibilityImportanceSetter;
-
+    private final Config mConfig = new Config();
     private final Selection mSelected = new Selection();
 
     private @Nullable ActionMode mActionMode;
     private @Nullable Menu mMenu;
 
-    private ActionModeController(
-            Context context,
+    public ActionModeController(
+            Activity activity,
             SelectionManager selectionMgr,
             MenuManager menuManager,
-            MenuManager.SelectionDetails selectionDetails,
-            Function<ActionMode.Callback, ActionMode> actionModeFactory,
-            EventHandler<MenuItem> menuItemClicker,
-            IntConsumer hapticPerformer,
-            Consumer<CharSequence> accessibilityAnnouncer,
-            AccessibilityImportanceSetter accessibilityImportanceSetter) {
-        mContext = context;
+            MessageBuilder messages) {
+
+        mActivity = activity;
         mSelectionMgr = selectionMgr;
         mMenuManager = menuManager;
-        mSelectionDetails = selectionDetails;
-
-        mActionModeFactory = actionModeFactory;
-        mMenuItemClicker = menuItemClicker;
-        mHapticPerformer = hapticPerformer;
-        mAccessibilityAnnouncer = accessibilityAnnouncer;
-        mAccessibilityImportanceSetter = accessibilityImportanceSetter;
+        mMessages = messages;
     }
 
     @Override
@@ -95,8 +79,8 @@
         if (mSelected.size() > 0) {
             if (mActionMode == null) {
                 if (DEBUG) Log.d(TAG, "Starting action mode.");
-                mActionMode = mActionModeFactory.apply(this);
-                mHapticPerformer.accept(HapticFeedbackConstants.LONG_PRESS);
+                mActionMode = mActivity.startActionMode(this);
+                mConfig.hapticPerformer.accept(HapticFeedbackConstants.LONG_PRESS);
             }
             updateActionMenu();
         } else {
@@ -108,10 +92,10 @@
 
         if (mActionMode != null) {
             assert(!mSelected.isEmpty());
-            final String title = Shared.getQuantityString(mContext,
+            final String title = mMessages.getQuantityString(
                     R.plurals.elements_selected, mSelected.size());
             mActionMode.setTitle(title);
-            mAccessibilityAnnouncer.accept(title);
+            mConfig.accessibilityAnnouncer.accept(title);
         }
     }
 
@@ -121,7 +105,7 @@
         if (mSelected.size() > 0) {
             if (mActionMode == null) {
                 if (DEBUG) Log.d(TAG, "Starting action mode.");
-                mActionMode = mActionModeFactory.apply(this);
+                mActionMode = mActivity.startActionMode(this);
             }
             updateActionMenu();
         } else {
@@ -133,10 +117,10 @@
 
         if (mActionMode != null) {
             assert(!mSelected.isEmpty());
-            final String title = Shared.getQuantityString(mContext,
+            final String title = mMessages.getQuantityString(
                     R.plurals.elements_selected, mSelected.size());
             mActionMode.setTitle(title);
-            mAccessibilityAnnouncer.accept(title);
+            mConfig.accessibilityAnnouncer.accept(title);
         }
     }
 
@@ -167,7 +151,7 @@
         mSelected.clear();
 
         // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
-        mAccessibilityImportanceSetter.setAccessibilityImportance(
+        mConfig.accessibilityImportanceSetter.setAccessibilityImportance(
                 View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, R.id.toolbar, R.id.roots_toolbar);
     }
 
@@ -181,7 +165,7 @@
 
             // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
             // these controls when using linear navigation.
-            mAccessibilityImportanceSetter.setAccessibilityImportance(
+            mConfig.accessibilityImportanceSetter.setAccessibilityImportance(
                     View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
                     R.id.toolbar,
                     R.id.roots_toolbar);
@@ -200,35 +184,13 @@
 
     private void updateActionMenu() {
         assert(mMenu != null);
-        mMenuManager.updateActionMenu(mMenu, mSelectionDetails);
+        mMenuManager.updateActionMenu(mMenu, mConfig.selectionDetails);
         Menus.disableHiddenItems(mMenu);
     }
 
     @Override
     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-        return mMenuItemClicker.accept(item);
-    }
-
-    static ActionModeController create(
-            Context context,
-            SelectionManager selectionMgr,
-            MenuManager menuManager,
-            MenuManager.SelectionDetails selectionDetails,
-            Activity activity,
-            View view,
-            EventHandler<MenuItem> menuItemClicker) {
-        return new ActionModeController(
-                context,
-                selectionMgr,
-                menuManager,
-                selectionDetails,
-                activity::startActionMode,
-                menuItemClicker,
-                view::performHapticFeedback,
-                view::announceForAccessibility,
-                (int accessibilityImportance, @IdRes int[] viewIds) -> {
-                    setImportantForAccessibility(activity, accessibilityImportance, viewIds);
-                });
+        return mConfig.menuItemClicker.accept(item);
     }
 
     private static void setImportantForAccessibility(
@@ -251,4 +213,30 @@
             finishActionMode();
         }
     }
+
+    public ActionModeController reset(
+            SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view) {
+        assert(mActionMode == null);
+        assert(mMenu == null);
+
+        mConfig.menuItemClicker = menuItemClicker;
+        mConfig.selectionDetails = selectionDetails;
+        mConfig.hapticPerformer = view::performHapticFeedback;
+        mConfig.accessibilityAnnouncer = view::announceForAccessibility;
+        mConfig.accessibilityImportanceSetter =
+                (int accessibilityImportance, @IdRes int[] viewIds) -> {
+                    setImportantForAccessibility(
+                            mActivity, accessibilityImportance, viewIds);
+                };
+
+        return this;
+    }
+
+    private static final class Config {
+        private EventHandler<MenuItem> menuItemClicker;
+        private SelectionDetails selectionDetails;
+        private IntConsumer hapticPerformer;
+        private Consumer<CharSequence> accessibilityAnnouncer;
+        private AccessibilityImportanceSetter accessibilityImportanceSetter;
+    }
 }
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index e7ab4b1..cb6ffbc 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -76,7 +76,6 @@
 import com.android.documentsui.RecentsLoader;
 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;
@@ -299,7 +298,7 @@
         final BaseActivity activity = getBaseActivity();
         mSelectionMgr = activity.getSelectionManager(mAdapter, this::canSetSelectionState);
         mFocusManager = activity.getFocusManager(mRecView, mModel);
-        mActions = activity.getActionHandler(mModel, mSelectionMgr, mConfig.mSearchMode);
+        mActions = activity.getActionHandler(mModel, mConfig.mSearchMode);
         mMenuManager = activity.getMenuManager();
         mDialogs = activity.getDialogController();
 
@@ -359,14 +358,11 @@
 
         mMenuManager = activity.getMenuManager();
 
-        mActionModeController = ActionModeController.create(
-                getContext(),
-                mSelectionMgr,
-                mMenuManager,
+        mActionModeController = activity.getActionModeController(
                 mSelectionMetadata,
-                getActivity(),
-                mRecView,
-                this::handleMenuItemClick);
+                this::handleMenuItemClick,
+                mRecView);
+
         mSelectionMgr.addCallback(mActionModeController);
 
         final ActivityManager am = (ActivityManager) context.getSystemService(
@@ -577,7 +573,7 @@
                 return true;
 
             case R.id.menu_open_in_new_window:
-                openInNewWindow(selection);
+                mActions.openSelectedInNewWindow();
                 return true;
 
             case R.id.menu_share:
@@ -589,8 +585,8 @@
             case R.id.menu_delete:
                 // deleteDocuments will end action mode if the documents are deleted.
                 // It won't end action mode if user cancels the delete.
-                mActions.deleteDocuments(
-                        mModel, selection, mActionModeController::finishOnConfirmed);
+                mActions.deleteSelectedDocuments(
+                        mModel, mActionModeController::finishOnConfirmed);
                 return true;
 
             case R.id.menu_copy_to:
@@ -682,16 +678,6 @@
         mActions.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()));
-        assert(doc != null);
-        mActions.openInNewWindow(new DocumentStack(getDisplayState().stack, doc));
-    }
-
     private void shareDocuments(final Selection selected) {
         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
 
@@ -742,9 +728,7 @@
 
     private boolean onDeleteSelectedDocuments() {
         if (mSelectionMgr.hasSelection()) {
-            Selection selection = mSelectionMgr.getSelection(new Selection());
-            mActions.deleteDocuments(
-                    mModel, selection, mActionModeController::finishOnConfirmed);
+            mActions.deleteSelectedDocuments(mModel, mActionModeController::finishOnConfirmed);
         }
         return false;
     }
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 7e94a29..e31d587 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.Metrics;
@@ -41,18 +42,15 @@
 import com.android.documentsui.clipping.ClipStore;
 import com.android.documentsui.clipping.DocumentClipper;
 import com.android.documentsui.clipping.UrisSupplier;
-import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
-import com.android.documentsui.selection.Selection;
-import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.files.ActionHandler.Addons;
 import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsAccess;
-import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.selection.Selection;
+import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.services.FileOperation;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.services.FileOperations;
@@ -72,31 +70,31 @@
     private static final String TAG = "ManagerActionHandler";
 
     private final DialogController mDialogs;
-    private final ActivityConfig mTuner;
+    private final ActivityConfig mActConfig;
     private final DocumentClipper mClipper;
     private final ClipStore mClipStore;
-
-    private final Config mConfig;
+    private final ContentScope mScope;
 
     ActionHandler(
             T activity,
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
+            SelectionManager selectionMgr,
             Lookup<String, Executor> executors,
             DialogController dialogs,
             ActivityConfig tuner,
             DocumentClipper clipper,
             ClipStore clipStore) {
 
-        super(activity, state, roots, docs, executors);
+        super(activity, state, roots, docs, selectionMgr, executors);
 
         mDialogs = dialogs;
-        mTuner = tuner;
+        mActConfig = tuner;
         mClipper = clipper;
         mClipStore = clipStore;
 
-        mConfig = new Config(this::onModelLoaded);
+        mScope = new ContentScope(this::onModelLoaded);
     }
 
     @Override
@@ -112,6 +110,15 @@
     }
 
     @Override
+    public void openSelectedInNewWindow() {
+        Selection selection = getStableSelection();
+        assert(selection.size() == 1);
+        DocumentInfo doc = mScope.model.getDocument(selection.iterator().next());
+        assert(doc != null);
+        openInNewWindow(new DocumentStack(mState.stack, doc));
+    }
+
+    @Override
     public void openSettings(RootInfo root) {
         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_SETTINGS);
         final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
@@ -143,16 +150,16 @@
 
     @Override
     public boolean openDocument(DocumentDetails details) {
-        DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
+        DocumentInfo doc = mScope.model.getDocument(details.getModelId());
         if (doc == null) {
             Log.w(TAG,
                     "Can't view item. No Document available for modeId: " + details.getModelId());
             return false;
         }
 
-        if (mTuner.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
+        if (mActConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
             onDocumentPicked(doc);
-            mConfig.selectionMgr.clearSelection();
+            mSelectionMgr.clearSelection();
             return true;
         }
         return false;
@@ -160,13 +167,13 @@
 
     @Override
     public boolean viewDocument(DocumentDetails details) {
-        DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
+        DocumentInfo doc = mScope.model.getDocument(details.getModelId());
         return viewDocument(doc);
     }
 
     @Override
     public boolean previewDocument(DocumentDetails details) {
-        DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
+        DocumentInfo doc = mScope.model.getDocument(details.getModelId());
         if (doc.isContainer()) {
             return false;
         }
@@ -174,16 +181,19 @@
     }
 
     @Override
-    public void deleteDocuments(Model model, Selection selected, ConfirmationCallback callback) {
+    public void deleteSelectedDocuments(Model model, ConfirmationCallback callback) {
+        assert(mSelectionMgr.hasSelection());
+
         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_DELETE);
 
-        assert(!selected.isEmpty());
+        Selection selection = getStableSelection();
+        assert(!selection.isEmpty());
 
         final DocumentInfo srcParent = mState.stack.peek();
         assert(srcParent != null);
 
         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
-        List<DocumentInfo> docs = model.getDocuments(selected);
+        List<DocumentInfo> docs = model.getDocuments(selection);
 
         ConfirmationCallback result = (@Result int code) -> {
             // share the news with our caller, be it good or bad.
@@ -196,7 +206,7 @@
             UrisSupplier srcs;
             try {
                 srcs = UrisSupplier.create(
-                        selected,
+                        selection,
                         model::getItemUri,
                         mClipStore);
             } catch (IOException e) {
@@ -371,7 +381,7 @@
                 mActivity.getPackageManager(),
                 mActivity.getResources(),
                 doc,
-                mConfig.model).build();
+                mScope.model).build();
 
         if (intent != null) {
             // TODO: un-work around issue b/24963914. Should be fixed soon.
@@ -456,49 +466,44 @@
 
     private void onModelLoaded(Model.Update update) {
         // When launched into empty root, open drawer.
-        if (mConfig.model.isEmpty()
+        if (mScope.model.isEmpty()
                 && !mState.hasInitialLocationChanged()
-                && !mConfig.searchMode
-                && !mConfig.modelLoadObserved) {
+                && !mScope.searchMode
+                && !mScope.modelLoadObserved) {
             // Opens the drawer *if* an openable drawer is present
             // else this is a no-op.
             mActivity.setRootsDrawerOpen(true);
         }
 
-        mConfig.modelLoadObserved = true;
+        mScope.modelLoadObserved = true;
     }
 
-    ActionHandler<T> reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
-        mConfig.reset(model, selectionMgr, searchMode);
+    ActionHandler<T> reset(Model model, boolean searchMode) {
+        assert(model != null);
+
+        mScope.model = model;
+        mScope.modelLoadObserved = false;
+        mScope.searchMode = searchMode;
+
+        model.addUpdateListener(mScope.modelUpdateListener);
+        mScope.modelLoadObserved = false;
+
         return this;
     }
 
-    private static final class Config {
+    private static final class ContentScope {
 
         @Nullable Model model;
-        @Nullable SelectionManager selectionMgr;
         boolean searchMode;
 
-        private final EventListener<Update> mModelUpdateListener;
+        private final EventListener<Update> modelUpdateListener;
 
         // We use this to keep track of whether a model has been previously loaded or not so we can
         // open the drawer on empty directories on first launch
         private boolean modelLoadObserved;
 
-        public Config(EventListener<Update> modelUpdateListener) {
-            mModelUpdateListener = modelUpdateListener;
-        }
-
-        public void reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
-            assert(model != null);
-
-            this.model = model;
-            this.selectionMgr = selectionMgr;
-            this.modelLoadObserved = false;
-            this.searchMode = searchMode;
-
-            model.addUpdateListener(mModelUpdateListener);
-            modelLoadObserved = false;
+        public ContentScope(EventListener<Update> modelUpdateListener) {
+            this.modelUpdateListener = modelUpdateListener;
         }
     }
 
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 0de0e26..448b7e9 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -25,29 +25,31 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.View;
 
+import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
+import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.OperationDialogFragment;
 import com.android.documentsui.OperationDialogFragment.DialogType;
 import com.android.documentsui.ProviderExecutor;
 import com.android.documentsui.R;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 import com.android.documentsui.clipping.DocumentClipper;
-import com.android.documentsui.ActivityConfig;
+import com.android.documentsui.dirlist.ActionModeController;
 import com.android.documentsui.dirlist.AnimationView.AnimationType;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.DocumentsAdapter;
@@ -72,10 +74,10 @@
     private final Config mConfig = new Config();
     private SelectionManager mSelectionMgr;
     private MenuManager mMenuManager;
-    private FocusManager mFocusManager;
     private ActionHandler<FilesActivity> mActions;
     private DialogController mDialogs;
     private DocumentClipper mClipper;
+    protected ActionModeController mActionModeController;
 
     public FilesActivity() {
         super(R.layout.files_activity, TAG);
@@ -97,20 +99,25 @@
                     }
                 });
 
-        // Make sure this is done after the RecyclerView and the Model are set up.
-        mFocusManager = new FocusManager(getColor(R.color.accent_dark));
-        mDialogs = DialogController.create(this);
+        mDialogs = DialogController.create(this, getMessages());
         mActions = new ActionHandler<>(
                 this,
                 mState,
                 mRoots,
                 DocumentsAccess.create(this),
+                mSelectionMgr,
                 ProviderExecutor::forAuthority,
                 mDialogs,
                 mConfig,
                 mClipper,
                 DocumentsApplication.getClipStore(this));
 
+        mActionModeController = new ActionModeController(
+                this,
+                mSelectionMgr,
+                mMenuManager,
+                getMessages());
+
         RootsFragment.show(getFragmentManager(), null);
 
         final Intent intent = getIntent();
@@ -354,26 +361,25 @@
     }
 
     @Override
-    public FocusManager getFocusManager(RecyclerView view, Model model) {
-        return mFocusManager.reset(view, model);
-    }
-
-    @Override
     public MenuManager getMenuManager() {
         return mMenuManager;
     }
 
     @Override
-    public ActionHandler<FilesActivity> getActionHandler(
-            Model model, SelectionManager selectionMgr, boolean searchMode) {
+    public final ActionModeController getActionModeController(
+            SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view) {
+        return mActionModeController.reset(selectionDetails, menuItemClicker, view);
+    }
+
+    @Override
+    public ActionHandler<FilesActivity> getActionHandler(Model model, boolean searchMode) {
 
         // provide our friend, RootsFragment, early access to this special feature!
-        if (model == null || selectionMgr == null) {
-            assert(model == null);
-            assert(selectionMgr == null);
+        if (model == null) {
             return mActions;
         }
-        return mActions.reset(model, selectionMgr, searchMode);
+
+        return mActions.reset(model, searchMode);
     }
 
     @Override
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index fbf4a43..50f45d1 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -57,21 +57,22 @@
 
     private static final String TAG = "PickerActionHandler";
 
-    private final ActivityConfig mActivityConfig;
-    private final Config mConfig;
+    private final ActivityConfig mConfig;
+    private final ContentScope mScope;
 
     ActionHandler(
             T activity,
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
+            SelectionManager selectionMgr,
             Lookup<String, Executor> executors,
             ActivityConfig activityConfig) {
 
-        super(activity, state, roots, docs, executors);
+        super(activity, state, roots, docs, selectionMgr, executors);
 
-        mActivityConfig = activityConfig;
-        mConfig = new Config(this::onModelLoaded);
+        mConfig = activityConfig;
+        mScope = new ContentScope(this::onModelLoaded);
     }
 
     @Override
@@ -133,16 +134,16 @@
 
     @Override
     public boolean openDocument(DocumentDetails details) {
-        DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
+        DocumentInfo doc = mScope.model.getDocument(details.getModelId());
         if (doc == null) {
             Log.w(TAG,
                     "Can't view item. No Document available for modeId: " + details.getModelId());
             return false;
         }
 
-        if (mActivityConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
+        if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) {
             mActivity.onDocumentPicked(doc);
-            mConfig.selectionMgr.clearSelection();
+            mSelectionMgr.clearSelection();
             return true;
         }
         return false;
@@ -162,49 +163,42 @@
         }
 
         // When launched into empty root, open drawer.
-        if (mConfig.model.isEmpty()) {
+        if (mScope.model.isEmpty()) {
             showDrawer = true;
         }
 
-        if (showDrawer && !mState.hasInitialLocationChanged() && !mConfig.searchMode
-                && !mConfig.modelLoadObserved) {
+        if (showDrawer && !mState.hasInitialLocationChanged() && !mScope.searchMode
+                && !mScope.modelLoadObserved) {
             // This noops on layouts without drawer, so no need to guard.
             mActivity.setRootsDrawerOpen(true);
         }
 
-        mConfig.modelLoadObserved = true;
+        mScope.modelLoadObserved = true;
     }
 
-    ActionHandler<T> reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
-        mConfig.reset(model, selectionMgr, searchMode);
+    ActionHandler<T> reset(Model model, boolean searchMode) {
+        assert(model != null);
+
+        mScope.model = model;
+        mScope.searchMode = searchMode;
+
+        model.addUpdateListener(mScope.modelUpdateListener);
         return this;
     }
 
-    private static final class Config {
+    private static final class ContentScope {
 
         @Nullable Model model;
-        @Nullable SelectionManager selectionMgr;
         boolean searchMode;
 
         // We use this to keep track of whether a model has been previously loaded or not so we can
         // open the drawer on empty directories on first launch
         private boolean modelLoadObserved;
 
-        private final EventListener<Update> mModelUpdateListener;
+        private final EventListener<Update> modelUpdateListener;
 
-        public Config(EventListener<Update> modelUpdateListener) {
-            mModelUpdateListener = modelUpdateListener;
-        }
-
-
-        public void reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
-            assert(model != null);
-
-            this.model = model;
-            this.selectionMgr = selectionMgr;
-            this.searchMode = searchMode;
-
-            model.addUpdateListener(mModelUpdateListener);
+        public ContentScope(EventListener<Update> modelUpdateListener) {
+            this.modelUpdateListener = modelUpdateListener;
         }
     }
 
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 2196ba9..12dd19b 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -38,24 +38,27 @@
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.support.design.widget.Snackbar;
-import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
 
+import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
+import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.ProviderExecutor;
 import com.android.documentsui.R;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.MimePredicate;
 import com.android.documentsui.base.PairedTask;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
-import com.android.documentsui.ActivityConfig;
+import com.android.documentsui.dirlist.ActionModeController;
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.dirlist.Model;
@@ -77,9 +80,9 @@
     private final Config mConfig = new Config();
 
     private SelectionManager mSelectionMgr;
-    private FocusManager mFocusManager;
     private MenuManager mMenuManager;
     private ActionHandler<PickActivity> mActionHandler;
+    private ActionModeController mActionModeController;
 
     public PickActivity() {
         super(R.layout.documents_activity, TAG);
@@ -93,16 +96,22 @@
                 mState.allowMultiple
                         ? SelectionManager.MODE_MULTIPLE
                         : SelectionManager.MODE_SINGLE);
-        mFocusManager = new FocusManager(getColor(R.color.accent_dark));
         mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
         mActionHandler = new ActionHandler<>(
                 this,
                 mState,
                 DocumentsApplication.getRootsCache(this),
                 DocumentsAccess.create(this),
+                mSelectionMgr,
                 ProviderExecutor::forAuthority,
                 mConfig);
 
+        mActionModeController = new ActionModeController(
+                this,
+                mSelectionMgr,
+                mMenuManager,
+                getMessages());
+
         Intent intent = getIntent();
 
         setupLayout(intent);
@@ -412,31 +421,27 @@
     }
 
     @Override
-    public FocusManager getFocusManager(RecyclerView view, Model model) {
-        return mFocusManager.reset(view, model);
-    }
-
-    @Override
     public MenuManager getMenuManager() {
         return mMenuManager;
     }
 
     @Override
-    public ActionHandler<PickActivity> getActionHandler(
-            Model model, SelectionManager selectionMgr, boolean searchMode) {
-
-        // provide our friend, RootsFragment, early access to this special feature!
-        if (model == null || selectionMgr == null) {
-            assert(model == null);
-            assert(selectionMgr == null);
-            return mActionHandler;
-        }
-        return mActionHandler.reset(model, selectionMgr, searchMode);
+    public ActionModeController getActionModeController(
+            SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker, View view) {
+        return mActionModeController.reset(selectionDetails, menuItemClicker, view);
     }
 
-    /* (non-Javadoc)
-     * @see com.android.documentsui.BaseActivity#getDialogController()
-     */
+    @Override
+    public ActionHandler<PickActivity> getActionHandler(Model model, boolean searchMode) {
+
+        // provide our friend, RootsFragment, early access to this special feature!
+        if (model == null) {
+            return mActionHandler;
+        }
+
+        return mActionHandler.reset(model, searchMode);
+    }
+
     @Override
     public DialogController getDialogController() {
         return DialogController.STUB;
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 2a4dca3..f6a4db3 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -47,6 +47,7 @@
 import android.widget.ListView;
 
 import com.android.documentsui.ActionHandler;
+import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.ItemDragListener;
@@ -59,7 +60,6 @@
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 import com.android.documentsui.roots.GetRootDocumentTask;
-import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.roots.RootsCache;
 import com.android.documentsui.roots.RootsLoader;
 
@@ -196,7 +196,7 @@
         final RootsCache roots = DocumentsApplication.getRootsCache(activity);
         final State state = activity.getDisplayState();
 
-        mActionHandler = activity.getActionHandler(null, null, false);
+        mActionHandler = activity.getActionHandler(null, false);
         mActivityConfig = activity.getActivityConfig();
 
         if (mActivityConfig.dragAndDropEnabled()) {
diff --git a/src/com/android/documentsui/ui/DialogController.java b/src/com/android/documentsui/ui/DialogController.java
index 90b7777..6c648a2 100644
--- a/src/com/android/documentsui/ui/DialogController.java
+++ b/src/com/android/documentsui/ui/DialogController.java
@@ -62,9 +62,9 @@
         private final Activity mActivity;
         private final MessageBuilder mMessages;
 
-        public RuntimeDialogController(Activity activity) {
+        public RuntimeDialogController(Activity activity, MessageBuilder messages) {
             mActivity = activity;
-            mMessages = new MessageBuilder(mActivity);
+            mMessages = messages;
         }
 
         @Override
@@ -139,7 +139,7 @@
         }
     }
 
-    static DialogController create(Activity activity) {
-        return new RuntimeDialogController(activity);
+    static DialogController create(Activity activity, MessageBuilder messages) {
+        return new RuntimeDialogController(activity, messages);
     }
 }
diff --git a/src/com/android/documentsui/ui/MessageBuilder.java b/src/com/android/documentsui/ui/MessageBuilder.java
index 7581006..dc3a638 100644
--- a/src/com/android/documentsui/ui/MessageBuilder.java
+++ b/src/com/android/documentsui/ui/MessageBuilder.java
@@ -15,6 +15,7 @@
  */
 package com.android.documentsui.ui;
 
+import android.annotation.PluralsRes;
 import android.content.Context;
 import android.text.BidiFormatter;
 
@@ -68,4 +69,11 @@
         }
         return message;
     }
+
+    /**
+     * Generates a formatted quantity string.
+     */
+    public String getQuantityString(@PluralsRes int stringId, int quantity) {
+        return Shared.getQuantityString(mContext, stringId, quantity);
+    }
 }
diff --git a/tests/common/com/android/documentsui/testing/MultiSelectManagers.java b/tests/common/com/android/documentsui/testing/SelectionManagers.java
similarity index 96%
rename from tests/common/com/android/documentsui/testing/MultiSelectManagers.java
rename to tests/common/com/android/documentsui/testing/SelectionManagers.java
index 3d0f253..c4c6c49 100644
--- a/tests/common/com/android/documentsui/testing/MultiSelectManagers.java
+++ b/tests/common/com/android/documentsui/testing/SelectionManagers.java
@@ -24,8 +24,8 @@
 import java.util.Collections;
 import java.util.List;
 
-public class MultiSelectManagers {
-    private MultiSelectManagers() {}
+public class SelectionManagers {
+    private SelectionManagers() {}
 
     public static SelectionManager createTestInstance() {
         return createTestInstance(Collections.emptyList());
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 08bee36..381b738 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -34,7 +34,13 @@
     }
 
     public TestActionHandler(TestEnv env) {
-        super(TestActivity.create(), env.state, env.roots, env.docs, (String authority) -> null);
+        super(
+                TestActivity.create(),
+                env.state,
+                env.roots,
+                env.docs,
+                env.selectionMgr,
+                (String authority) -> null);
     }
 
     @Override
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 10de113..2f8587b 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -22,6 +22,7 @@
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.TestModel;
+import com.android.documentsui.selection.SelectionManager;
 
 import junit.framework.Assert;
 
@@ -48,10 +49,12 @@
     public final TestRootsAccess roots = new TestRootsAccess();
     public final TestDocumentsAccess docs = new TestDocumentsAccess();
     public final TestModel model;
+    public final SelectionManager selectionMgr;
 
     private TestEnv(String authority) {
         mExecutor = new TestScheduledExecutorService();
         model = new TestModel(authority);
+        selectionMgr = SelectionManagers.createTestInstance();
     }
 
     public static TestEnv create() {
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index d924a44..254fdbf 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -55,6 +55,7 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
+                mEnv.selectionMgr,
                 mEnv::lookupExecutor) {
 
             @Override
diff --git a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
index 6ad40bb..c2bd556 100644
--- a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
@@ -27,7 +27,7 @@
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.selection.Selection;
 import com.android.documentsui.testing.TestEvent;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.Views;
 
 import java.util.ArrayList;
@@ -43,7 +43,7 @@
 
     @Override
     public void setUp() throws Exception {
-        mMultiSelectManager = MultiSelectManagers.createTestInstance();
+        mMultiSelectManager = SelectionManagers.createTestInstance();
 
         mListener = new DragStartListener.ActiveListener(
                 new State(),
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_KeyboardTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_KeyboardTest.java
index e8b1d4a..0777f2f 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_KeyboardTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_KeyboardTest.java
@@ -23,7 +23,7 @@
 
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.TestActionHandler;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEventHandler;
@@ -58,7 +58,7 @@
 
     @Before
     public void setUp() {
-        SelectionManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
+        SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
 
         mActionHandler = new TestActionHandler();
         mSelection = new SelectionProbe(selectionMgr);
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
index a77b2d8..787652a 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -25,7 +25,7 @@
 
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.TestActionHandler;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
@@ -59,7 +59,7 @@
     @Before
     public void setUp() {
 
-        SelectionManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
+        SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
         mActionHandler = new TestActionHandler();
 
         mSelection = new SelectionProbe(selectionMgr);
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
index 28aae70..c34322e 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
@@ -22,7 +22,7 @@
 
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.TestActionHandler;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
@@ -60,7 +60,7 @@
     @Before
     public void setUp() {
 
-        SelectionManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
+        SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
         mActionHandler = new TestActionHandler();
 
         mSelection = new SelectionProbe(selectionMgr);
diff --git a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
index 2eebc8c..f8ad916 100644
--- a/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
@@ -25,7 +25,7 @@
 
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.TestActionHandler;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
@@ -59,7 +59,7 @@
 
     @Before
     public void setUp() {
-        SelectionManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
+        SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
 
         mActionHandler = new TestActionHandler();
 
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 5ec0e35..1583b41 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -21,14 +21,17 @@
 
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.R;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.selection.Selection;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.testing.Roots;
 import com.android.documentsui.testing.TestConfirmationCallback;
 import com.android.documentsui.testing.TestEnv;
 import com.android.documentsui.testing.TestRootsAccess;
@@ -47,7 +50,6 @@
     private TestDialogController mDialogs;
     private TestConfirmationCallback mCallback;
     private ActionHandler<TestActivity> mHandler;
-    private Selection mSelection;
 
     @Before
     public void setUp() {
@@ -62,6 +64,7 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
+                mEnv.selectionMgr,
                 mEnv::lookupExecutor,
                 mDialogs,
                 null,  // tuner, not currently used.
@@ -71,10 +74,22 @@
 
         mDialogs.confirmNext();
 
-        mSelection = new Selection();
-        mSelection.add("1");
+        mEnv.selectionMgr.toggleSelection("1");
 
-        mHandler.reset(mEnv.model, null, false);
+        mHandler.reset(mEnv.model, false);
+    }
+
+    @Test
+    public void testOpenSelectedInNewWindow() {
+        mHandler.openSelectedInNewWindow();
+
+        DocumentStack path = new DocumentStack(Roots.create("123"), mEnv.model.getDocument("1"));
+
+        Intent expected = LauncherActivity.createLaunchIntent(mActivity);
+        expected.putExtra(Shared.EXTRA_STACK, (Parcelable) path);
+
+        Intent actual = mActivity.startActivity.getLastValue();
+        assertEquals(expected.toString(), actual.toString());
     }
 
     @Test
@@ -91,7 +106,7 @@
     public void testDeleteDocuments() {
         mEnv.populateStack();
 
-        mHandler.deleteDocuments(mEnv.model, mSelection, mCallback);
+        mHandler.deleteSelectedDocuments(mEnv.model, mCallback);
         mDialogs.assertNoFileFailures();
         mActivity.startService.assertCalled();
         mCallback.assertConfirmed();
@@ -102,7 +117,7 @@
         mEnv.populateStack();
 
         mDialogs.rejectNext();
-        mHandler.deleteDocuments(mEnv.model, mSelection, mCallback);
+        mHandler.deleteSelectedDocuments(mEnv.model, mCallback);
         mDialogs.assertNoFileFailures();
         mActivity.startService.assertNotCalled();
         mCallback.assertRejected();
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index 5f1d567..35199c3 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -18,7 +18,6 @@
 
 import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
@@ -30,7 +29,6 @@
 import com.android.documentsui.R;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
-import com.android.documentsui.selection.Selection;
 import com.android.documentsui.testing.TestEnv;
 import com.android.documentsui.testing.TestRootsAccess;
 import com.android.documentsui.ui.TestDialogController;
@@ -47,7 +45,6 @@
     private TestActivity mActivity;
     private TestDialogController mDialogs;
     private ActionHandler<TestActivity> mHandler;
-    private Selection mSelection;
 
     @Before
     public void setUp() {
@@ -61,16 +58,16 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
+                mEnv.selectionMgr,
                 mEnv::lookupExecutor,
                 null  // tuner, not currently used.
                 );
 
         mDialogs.confirmNext();
 
-        mSelection = new Selection();
-        mSelection.add("1");
+        mEnv.selectionMgr.toggleSelection("1");
 
-        mHandler.reset(mEnv.model, null, false);
+        mHandler.reset(mEnv.model, false);
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java b/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java
index 6b54577..d522efd 100644
--- a/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java
+++ b/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java
@@ -23,7 +23,7 @@
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.dirlist.TestData;
 import com.android.documentsui.selection.Selection;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 import com.android.documentsui.testing.dirlist.TestSelectionListener;
 
@@ -50,7 +50,7 @@
     @Before
     public void setUp() throws Exception {
         mCallback = new TestSelectionListener();
-        mManager = MultiSelectManagers.createTestInstance(
+        mManager = SelectionManagers.createTestInstance(
                 ITEMS,
                 SelectionManager.MODE_MULTIPLE,
                 (String id, boolean nextState) -> (!nextState || !mIgnored.contains(id)));
diff --git a/tests/unit/com/android/documentsui/selection/SelectionManager_SingleSelectTest.java b/tests/unit/com/android/documentsui/selection/SelectionManager_SingleSelectTest.java
index e659d49..d2a546c 100644
--- a/tests/unit/com/android/documentsui/selection/SelectionManager_SingleSelectTest.java
+++ b/tests/unit/com/android/documentsui/selection/SelectionManager_SingleSelectTest.java
@@ -24,7 +24,7 @@
 import com.android.documentsui.dirlist.TestData;
 import com.android.documentsui.dirlist.TestDocumentsAdapter;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.testing.MultiSelectManagers;
+import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 import com.android.documentsui.testing.dirlist.TestSelectionListener;
 
@@ -48,7 +48,7 @@
     @Before
     public void setUp() throws Exception {
         mCallback = new TestSelectionListener();
-        mManager = MultiSelectManagers.createTestInstance(ITEMS, SelectionManager.MODE_SINGLE);
+        mManager = SelectionManagers.createTestInstance(ITEMS, SelectionManager.MODE_SINGLE);
         mManager.addCallback(mCallback);
 
         mSelection = new SelectionProbe(mManager);