Merge "Refactor SelectionModeListener." into nyc-andromeda-dev
diff --git a/app-perf-tests/src/com/android/documentsui/LauncherActivity.java b/app-perf-tests/src/com/android/documentsui/LauncherActivity.java
index 21fc52e..21ea8aa 100644
--- a/app-perf-tests/src/com/android/documentsui/LauncherActivity.java
+++ b/app-perf-tests/src/com/android/documentsui/LauncherActivity.java
@@ -20,10 +20,8 @@
 
 import android.app.Activity;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Handler;
-import android.util.Log;
 
 import java.util.concurrent.CountDownLatch;
 
diff --git a/src/com/android/documentsui/Events.java b/src/com/android/documentsui/Events.java
index c0e3d7d..0de4039 100644
--- a/src/com/android/documentsui/Events.java
+++ b/src/com/android/documentsui/Events.java
@@ -336,9 +336,4 @@
                     .toString();
         }
     }
-
-    @FunctionalInterface
-    public interface EventHandler {
-        boolean apply(InputEvent event);
-    }
 }
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index 8b2880d..19d4482 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -16,8 +16,6 @@
 
 package com.android.documentsui;
 
-import android.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
 import android.view.Menu;
 import android.view.MenuItem;
 
diff --git a/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/src/com/android/documentsui/OpenExternalDirectoryActivity.java
index 6588ee1..245f902 100644
--- a/src/com/android/documentsui/OpenExternalDirectoryActivity.java
+++ b/src/com/android/documentsui/OpenExternalDirectoryActivity.java
@@ -17,12 +17,10 @@
 package com.android.documentsui;
 
 import static android.os.Environment.isStandardDirectory;
-import static android.os.Environment.STANDARD_DIRECTORIES;
 import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
 import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;
 
 import static com.android.documentsui.LocalPreferences.getScopedAccessPermissionStatus;
-import static com.android.documentsui.LocalPreferences.PERMISSION_ASK;
 import static com.android.documentsui.LocalPreferences.PERMISSION_ASK_AGAIN;
 import static com.android.documentsui.LocalPreferences.PERMISSION_NEVER_ASK;
 import static com.android.documentsui.LocalPreferences.setScopedAccessPermissionStatus;
diff --git a/src/com/android/documentsui/QuickViewIntentBuilder.java b/src/com/android/documentsui/QuickViewIntentBuilder.java
index 5e3bbbb..b8d2247 100644
--- a/src/com/android/documentsui/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/QuickViewIntentBuilder.java
@@ -22,7 +22,6 @@
 
 import android.content.ClipData;
 import android.content.ClipDescription;
-import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
diff --git a/src/com/android/documentsui/base/FunctionalInterfaces.java b/src/com/android/documentsui/base/FunctionalInterfaces.java
new file mode 100644
index 0000000..936883a
--- /dev/null
+++ b/src/com/android/documentsui/base/FunctionalInterfaces.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * A container class that contains common functional interfaces used in DocumentsUI.
+ *
+ * This class should never be instantiated.
+ */
+public class FunctionalInterfaces {
+
+    private FunctionalInterfaces() {}
+
+    /**
+     * A functional interface that handles an event and returns a boolean to indicate if the event
+     * is consumed.
+     */
+    @FunctionalInterface
+    public interface EventHandler<T> {
+        boolean apply(T event);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/ActionModeController.java b/src/com/android/documentsui/dirlist/ActionModeController.java
new file mode 100644
index 0000000..8038768
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/ActionModeController.java
@@ -0,0 +1,237 @@
+/*
+ * 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.dirlist;
+
+import static com.android.documentsui.Shared.DEBUG;
+
+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;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.android.documentsui.MenuManager;
+import com.android.documentsui.Menus;
+import com.android.documentsui.R;
+import com.android.documentsui.base.FunctionalInterfaces.EventHandler;
+import com.android.documentsui.Shared;
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.IntConsumer;
+
+/**
+ * A controller that listens to selection changes and manages life cycles of action modes.
+ */
+class ActionModeController implements MultiSelectManager.Callback, ActionMode.Callback {
+
+    private static final String TAG = "ActionModeController";
+
+    private final Context mContext;
+    private final MultiSelectManager mSelectionMgr;
+    private final MenuManager mMenuManager;
+    private final MenuManager.SelectionDetails mSelectionDetails;
+
+    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 Selection mSelected = new Selection();
+
+    private @Nullable ActionMode mActionMode;
+    private @Nullable Menu mMenu;
+
+    private ActionModeController(
+            Context context,
+            MultiSelectManager selectionMgr,
+            MenuManager menuManager,
+            MenuManager.SelectionDetails selectionDetails,
+            Function<ActionMode.Callback, ActionMode> actionModeFactory,
+            EventHandler<MenuItem> menuItemClicker,
+            IntConsumer hapticPerformer,
+            Consumer<CharSequence> accessibilityAnnouncer,
+            AccessibilityImportanceSetter accessibilityImportanceSetter) {
+        mContext = context;
+        mSelectionMgr = selectionMgr;
+        mMenuManager = menuManager;
+        mSelectionDetails = selectionDetails;
+
+        mActionModeFactory = actionModeFactory;
+        mMenuItemClicker = menuItemClicker;
+        mHapticPerformer = hapticPerformer;
+        mAccessibilityAnnouncer = accessibilityAnnouncer;
+        mAccessibilityImportanceSetter = accessibilityImportanceSetter;
+    }
+
+    @Override
+    public void onSelectionChanged() {
+        mSelectionMgr.getSelection(mSelected);
+        if (mSelected.size() > 0) {
+            if (mActionMode == null) {
+                if (DEBUG) Log.d(TAG, "Starting action mode.");
+                mActionMode = mActionModeFactory.apply(this);
+                mHapticPerformer.accept(HapticFeedbackConstants.LONG_PRESS);
+            }
+            updateActionMenu();
+        } else {
+            if (mActionMode != null) {
+                if (DEBUG) Log.d(TAG, "Finishing action mode.");
+                mActionMode.finish();
+            }
+        }
+
+        if (mActionMode != null) {
+            assert(!mSelected.isEmpty());
+            final String title = Shared.getQuantityString(mContext,
+                    R.plurals.elements_selected, mSelected.size());
+            mActionMode.setTitle(title);
+            mAccessibilityAnnouncer.accept(title);
+        }
+    }
+
+    @Override
+    public void onSelectionRestored() {
+        mSelectionMgr.getSelection(mSelected);
+        if (mSelected.size() > 0) {
+            if (mActionMode == null) {
+                if (DEBUG) Log.d(TAG, "Starting action mode.");
+                mActionMode = mActionModeFactory.apply(this);
+            }
+            updateActionMenu();
+        } else {
+            if (mActionMode != null) {
+                if (DEBUG) Log.d(TAG, "Finishing action mode.");
+                mActionMode.finish();
+            }
+        }
+
+        if (mActionMode != null) {
+            assert(!mSelected.isEmpty());
+            final String title = Shared.getQuantityString(mContext,
+                    R.plurals.elements_selected, mSelected.size());
+            mActionMode.setTitle(title);
+            mAccessibilityAnnouncer.accept(title);
+        }
+    }
+
+    void finishActionMode() {
+        if (mActionMode != null) {
+            mActionMode.finish();
+            mActionMode = null;
+        } else {
+            Log.w(TAG, "Tried to finish a null action mode.");
+        }
+    }
+
+    // Called when the user exits the action mode
+    @Override
+    public void onDestroyActionMode(ActionMode mode) {
+        if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
+        mActionMode = null;
+        // clear selection
+        mSelectionMgr.clearSelection();
+        mSelected.clear();
+
+        // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
+        mAccessibilityImportanceSetter.setAccessibilityImportance(
+                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, R.id.toolbar, R.id.roots_toolbar);
+    }
+
+    @Override
+    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+        int size = mSelectionMgr.getSelection().size();
+        mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
+        mode.setTitle(TextUtils.formatSelectedCount(size));
+
+        if (size > 0) {
+
+            // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
+            // these controls when using linear navigation.
+            mAccessibilityImportanceSetter.setAccessibilityImportance(
+                    View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
+                    R.id.toolbar,
+                    R.id.roots_toolbar);
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+        mMenu = menu;
+        updateActionMenu();
+        return true;
+    }
+
+    private void updateActionMenu() {
+        assert(mMenu != null);
+        mMenuManager.updateActionMenu(mMenu, mSelectionDetails);
+        Menus.disableHiddenItems(mMenu);
+    }
+
+    @Override
+    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+        return mMenuItemClicker.apply(item);
+    }
+
+    static ActionModeController create(
+            Context context,
+            MultiSelectManager 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);
+                });
+    }
+
+    private static void setImportantForAccessibility(
+            Activity activity, int accessibilityImportance, @IdRes int[] viewIds) {
+        for (final int id : viewIds) {
+            final View v = activity.findViewById(id);
+            if (v != null) {
+                v.setImportantForAccessibility(accessibilityImportance);
+            }
+        }
+    }
+
+    @FunctionalInterface
+    private interface AccessibilityImportanceSetter {
+        void setAccessibilityImportance(int accessibilityImportance, @IdRes int... viewIds);
+    }
+}
diff --git a/src/com/android/documentsui/dirlist/BandController.java b/src/com/android/documentsui/dirlist/BandController.java
index 1257303..9190aae 100644
--- a/src/com/android/documentsui/dirlist/BandController.java
+++ b/src/com/android/documentsui/dirlist/BandController.java
@@ -33,12 +33,9 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
-import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.documentsui.Events;
 import com.android.documentsui.Events.InputEvent;
-import com.android.documentsui.Events.MotionInputEvent;
 import com.android.documentsui.R;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
@@ -328,7 +325,7 @@
     }
 
     private boolean onBeforeItemStateChange(String id, boolean nextState) {
-        return mSelectionManager.notifyBeforeItemStateChange(id, nextState);
+        return mSelectionManager.canSetState(id, nextState);
     }
 
     @Override
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 30ccb28..fe07958 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -50,13 +50,10 @@
 import android.support.v7.widget.RecyclerView.RecyclerListener;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.text.BidiFormatter;
-import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
-import android.view.ActionMode;
 import android.view.ContextMenu;
 import android.view.DragEvent;
-import android.view.HapticFeedbackConstants;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -66,7 +63,6 @@
 import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.TextView;
-import android.widget.Toolbar;
 
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DirectoryLoader;
@@ -77,10 +73,8 @@
 import com.android.documentsui.Events.MotionInputEvent;
 import com.android.documentsui.ItemDragListener;
 import com.android.documentsui.MenuManager;
-import com.android.documentsui.Menus;
 import com.android.documentsui.MessageBar;
 import com.android.documentsui.Metrics;
-import com.android.documentsui.MimePredicate;
 import com.android.documentsui.R;
 import com.android.documentsui.RecentsLoader;
 import com.android.documentsui.RetainedState;
@@ -108,7 +102,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 
 import javax.annotation.Nullable;
 
@@ -143,8 +136,9 @@
 
     private final Model mModel = new Model();
     private final Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
-    private final SelectionModeListener mSelectionModeListener = new SelectionModeListener();
     private MultiSelectManager mSelectionMgr;
+    private ActionModeController mActionModeController;
+    private SelectionMetadata mSelectionMetadata;
     private UserInputHandler<InputEvent> mInputHandler;
     private FocusManager mFocusManager;
 
@@ -183,7 +177,6 @@
     private boolean mSearchMode = false;
 
     private @Nullable BandController mBandController;
-    private @Nullable ActionMode mActionMode;
 
     private DragHoverListener mDragHoverListener;
     private MenuManager mMenuManager;
@@ -301,7 +294,10 @@
                 mAdapter,
                 state.allowMultiple
                     ? MultiSelectManager.MODE_MULTIPLE
-                    : MultiSelectManager.MODE_SINGLE);
+                    : MultiSelectManager.MODE_SINGLE,
+                this::canSetSelectionState);
+        mSelectionMetadata = new SelectionMetadata(mSelectionMgr, mModel::getItem);
+        mSelectionMgr.addItemCallback(mSelectionMetadata);
 
         mModel.addUpdateListener(mAdapter);
         mModel.addUpdateListener(mModelUpdateListener);
@@ -355,7 +351,19 @@
                 mInputHandler,
                 mBandController);
 
-        mSelectionMgr.addCallback(mSelectionModeListener);
+        final BaseActivity activity = getBaseActivity();
+        mTuner = activity.createFragmentTuner();
+        mMenuManager = activity.getMenuManager();
+
+        mActionModeController = ActionModeController.create(
+                getContext(),
+                mSelectionMgr,
+                mMenuManager,
+                mSelectionMetadata,
+                getActivity(),
+                mRecView,
+                this::handleMenuItemClick);
+        mSelectionMgr.addCallback(mActionModeController);
 
         final ActivityManager am = (ActivityManager) context.getSystemService(
                 Context.ACTIVITY_SERVICE);
@@ -440,7 +448,9 @@
         boolean mouseOverFile = !(v == mRecView || v == mEmptyView);
         if (mouseOverFile) {
             mMenuManager.updateContextMenuForFile(
-                    menu, mSelectionModeListener, getBaseActivity().getDirectoryDetails());
+                    menu,
+                    mSelectionMetadata,
+                    getBaseActivity().getDirectoryDetails());
         } else {
            mMenuManager.updateContextMenuForContainer(
                    menu, getBaseActivity().getDirectoryDetails());
@@ -593,220 +603,19 @@
         return (BaseActivity) getActivity();
     }
 
-    /**
-     * Manages the integration between our ActionMode and MultiSelectManager, initiating
-     * ActionMode when there is a selection, canceling it when there is no selection,
-     * and clearing selection when action mode is explicitly exited by the user.
-     */
-    private final class SelectionModeListener implements MultiSelectManager.Callback,
-            ActionMode.Callback, MenuManager.SelectionDetails {
-
-        private Selection mSelected = new Selection();
-
-        // Partial files are files that haven't been fully downloaded.
-        private int mPartialCount = 0;
-        private int mDirectoryCount = 0;
-        private int mWritableDirectoryCount = 0;
-        private int mNoDeleteCount = 0;
-        private int mNoRenameCount = 0;
-
-        private Menu mMenu;
-
-        @Override
-        public boolean onBeforeItemStateChange(String modelId, boolean selected) {
-            if (selected) {
-                final Cursor cursor = mModel.getItem(modelId);
-                if (cursor == null) {
-                    Log.w(TAG, "Can't obtain cursor for modelId: " + modelId);
-                    return false;
-                }
-
-                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-                final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-                if (!mTuner.canSelectType(docMimeType, docFlags)) {
-                    return false;
-                }
-                return mTuner.canSelectType(docMimeType, docFlags);
-            }
-            return true;
-        }
-
-        @Override
-        public void onItemStateChanged(String modelId, boolean selected) {
-            final Cursor cursor = mModel.getItem(modelId);
-            if (cursor == null) {
-                Log.w(TAG, "Model returned null cursor for document: " + modelId
-                        + ". Ignoring state changed event.");
-                return;
-            }
-
-            // TODO: Should this be happening in onSelectionChanged? Technically this callback is
-            // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized
-            // selection changes here)
-            final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-            if (MimePredicate.isDirectoryType(mimeType)) {
-                mDirectoryCount += selected ? 1 : -1;
-            }
-
-            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-            if ((docFlags & Document.FLAG_PARTIAL) != 0) {
-                mPartialCount += selected ? 1 : -1;
-            }
-            if ((docFlags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0) {
-                mWritableDirectoryCount += selected ? 1 : -1;
-            }
-            if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
-                mNoDeleteCount += selected ? 1 : -1;
-            }
-            if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) {
-                mNoRenameCount += selected ? 1 : -1;
-            }
-        }
-
-        @Override
-        public void onSelectionChanged() {
-            mSelectionMgr.getSelection(mSelected);
-            if (mSelected.size() > 0) {
-                 if (mActionMode == null) {
-                    if (DEBUG) Log.d(TAG, "Starting action mode.");
-                    mActionMode = getActivity().startActionMode(this);
-                }
-                updateActionMenu();
-            } else {
-                if (mActionMode != null) {
-                    if (DEBUG) Log.d(TAG, "Finishing action mode.");
-                    mActionMode.finish();
-                }
-            }
-
-            if (mActionMode != null) {
-                assert(!mSelected.isEmpty());
-                final String title = Shared.getQuantityString(getActivity(),
-                        R.plurals.elements_selected, mSelected.size());
-                mActionMode.setTitle(title);
-                mRecView.announceForAccessibility(title);
-            }
-        }
-
-        // Called when the user exits the action mode
-        @Override
-        public void onDestroyActionMode(ActionMode mode) {
-            if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
-            mActionMode = null;
-            // clear selection
-            mSelectionMgr.clearSelection();
-            mSelected.clear();
-
-            mDirectoryCount = 0;
-            mPartialCount = 0;
-            mNoDeleteCount = 0;
-            mNoRenameCount = 0;
-
-            // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode.
-            final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
-            toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-
-            // This toolbar is not present in the fixed_layout
-            final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar);
-            if (rootsToolbar != null) {
-                rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-            }
-        }
-
-        @Override
-        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-            if (mRestoredSelection != null) {
-                // This is a careful little song and dance to avoid haptic feedback
-                // when selection has been restored after rotation. We're
-                // also responsible for cleaning up restored selection so the
-                // object dones't unnecessarily hang around.
-                mRestoredSelection = null;
-            } else {
-                mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-            }
-
-            int size = mSelectionMgr.getSelection().size();
-            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
-            mode.setTitle(TextUtils.formatSelectedCount(size));
-
-            if (size > 0) {
-                // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to
-                // these controls when using linear navigation.
-                final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar);
-                toolbar.setImportantForAccessibility(
-                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-
-                // This toolbar is not present in the fixed_layout
-                final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(
-                        R.id.roots_toolbar);
-                if (rootsToolbar != null) {
-                    rootsToolbar.setImportantForAccessibility(
-                            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-                }
-                return true;
-            }
-
-            return false;
-        }
-
-        @Override
-        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-            mMenu = menu;
-            updateActionMenu();
-            return true;
-        }
-
-        @Override
-        public boolean containsDirectories() {
-            return mDirectoryCount > 0;
-        }
-
-        @Override
-        public boolean containsPartialFiles() {
-            return mPartialCount > 0;
-        }
-
-        @Override
-        public boolean canDelete() {
-            return mNoDeleteCount == 0;
-        }
-
-        @Override
-        public boolean canRename() {
-            return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1;
-        }
-
-        @Override
-        public boolean canPasteInto() {
-            return mDirectoryCount == 1 && mWritableDirectoryCount == 1
-                    && mSelectionMgr.getSelection().size() == 1;
-        }
-
-        private void updateActionMenu() {
-            assert(mMenu != null);
-            mMenuManager.updateActionMenu(mMenu, this);
-            Menus.disableHiddenItems(mMenu);
-        }
-
-        @Override
-        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            return handleMenuItemClick(item);
-        }
-    }
-
     private boolean handleMenuItemClick(MenuItem item) {
         Selection selection = mSelectionMgr.getSelection(new Selection());
 
         switch (item.getItemId()) {
             case R.id.menu_open:
                 openDocuments(selection);
-                mActionMode.finish();
+                mActionModeController.finishActionMode();
                 return true;
 
             case R.id.menu_share:
                 shareDocuments(selection);
                 // TODO: Only finish selection if share action is completed.
-                mActionMode.finish();
+                mActionModeController.finishActionMode();
                 return true;
 
             case R.id.menu_delete:
@@ -819,12 +628,12 @@
                 transferDocuments(selection, FileOperationService.OPERATION_COPY);
                 // TODO: Only finish selection mode if copy-to is not canceled.
                 // Need to plum down into handling the way we do with deleteDocuments.
-                mActionMode.finish();
+                mActionModeController.finishActionMode();
                 return true;
 
             case R.id.menu_move_to:
                 // Exit selection mode first, so we avoid deselecting deleted documents.
-                mActionMode.finish();
+                mActionModeController.finishActionMode();
                 transferDocuments(selection, FileOperationService.OPERATION_MOVE);
                 return true;
 
@@ -851,7 +660,7 @@
             case R.id.menu_rename:
                 // Exit selection mode first, so we avoid deselecting deleted
                 // (renamed) documents.
-                mActionMode.finish();
+                mActionModeController.finishActionMode();
                 renameDocuments(selection);
                 return true;
 
@@ -1013,11 +822,7 @@
                         // This is done here, rather in the onActionItemClicked
                         // so we can avoid de-selecting items in the case where
                         // the user cancels the delete.
-                        if (mActionMode != null) {
-                            mActionMode.finish();
-                        } else {
-                            Log.w(TAG, "Action mode is null before deleting documents.");
-                        }
+                        mActionModeController.finishActionMode();
 
                         UrisSupplier srcs;
                         try {
@@ -1312,9 +1117,7 @@
         // When files are selected for dragging, ActionMode is started. This obscures the breadcrumb
         // with an ActionBar. In order to make drag and drop to the breadcrumb possible, we first
         // end ActionMode so the breadcrumb is visible to the user.
-        if (mActionMode != null) {
-            mActionMode.finish();
-        }
+        mActionModeController.finishActionMode();
     }
 
     void dragStopped(boolean result) {
@@ -1475,22 +1278,25 @@
     }
 
     private boolean canSelect(DocumentDetails doc) {
-        return canSelect(doc.getModelId());
+        return canSetSelectionState(doc.getModelId(), true);
     }
 
-    private boolean canSelect(String modelId) {
+    private boolean canSetSelectionState(String modelId, boolean nextState) {
+        if (nextState) {
+            // Check if an item can be selected
+            final Cursor cursor = mModel.getItem(modelId);
+            if (cursor == null) {
+                Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId);
+                return false;
+            }
 
-        // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost
-        // the same, and responsible for the same thing (whether to select or not).
-        final Cursor cursor = mModel.getItem(modelId);
-        if (cursor == null) {
-            Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId);
-            return false;
+            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+            return mTuner.canSelectType(docMimeType, docFlags);
+        } else {
+            // Right now all selected items can be deselected.
+            return true;
         }
-
-        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-        return mTuner.canSelectType(docMimeType, docFlags);
     }
 
     public static void showDirectory(
diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
index 4b35447..cb5afd7 100644
--- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
@@ -35,7 +35,7 @@
  * dummy layout objects was error prone when interspersed with the core mode / adapter code.
  *
  * @see ModelBackedDocumentsAdapter
- * @see SectionBreakDocumentsAdapter
+ * @see SectionBreakDocumentsAdapterWrapper
  */
 abstract class DocumentsAdapter
         extends RecyclerView.Adapter<DocumentHolder>
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index 4cf8455..514c030 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -26,9 +26,9 @@
 import android.view.View.OnTouchListener;
 
 import com.android.documentsui.Events;
-import com.android.documentsui.Events.EventHandler;
 import com.android.documentsui.Events.InputEvent;
 import com.android.documentsui.Events.MotionInputEvent;
+import com.android.documentsui.base.FunctionalInterfaces.EventHandler;
 
 //Receives event meant for both directory and empty view, and either pass them to
 //{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for
@@ -37,7 +37,7 @@
         implements OnItemTouchListener, OnTouchListener {
 
     private final GestureSelector mGestureSelector;
-    private final EventHandler mMouseDragListener;
+    private final EventHandler<InputEvent> mMouseDragListener;
     private final BandController mBandController;
     private final MouseDelegate mMouseDelegate = new MouseDelegate();
     private final TouchDelegate mTouchDelegate = new TouchDelegate();
@@ -46,7 +46,7 @@
             Context context,
             RecyclerView recView,
             View emptyView,
-            EventHandler mouseDragListener,
+            EventHandler<InputEvent> mouseDragListener,
             GestureSelector gestureSelector,
             UserInputHandler<? extends InputEvent> handler,
             @Nullable BandController bandController) {
diff --git a/src/com/android/documentsui/dirlist/MultiSelectManager.java b/src/com/android/documentsui/dirlist/MultiSelectManager.java
index 0a2cd42..780c564 100644
--- a/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -68,17 +68,25 @@
     private final Selection mSelection = new Selection();
 
     private final DocumentsAdapter mAdapter;
-    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
+    private final List<Callback> mCallbacks = new ArrayList<>(1);
+    private final List<ItemCallback> mItemCallbacks = new ArrayList<>(1);
 
     private @Nullable Range mRanger;
     private boolean mSingleSelect;
 
-    public MultiSelectManager(DocumentsAdapter adapter, @SelectionMode int mode) {
+    private final SelectionPredicate mCanSetState;
+
+    public MultiSelectManager(
+            DocumentsAdapter adapter,
+            @SelectionMode int mode,
+            SelectionPredicate canSetState) {
 
         assert(adapter != null);
 
         mAdapter = adapter;
 
+        mCanSetState = canSetState;
+
         mSingleSelect = mode == MODE_SINGLE;
         mAdapter.registerAdapterDataObserver(
                 new RecyclerView.AdapterDataObserver() {
@@ -133,10 +141,16 @@
      *
      * @param callback
      */
-    public void addCallback(MultiSelectManager.Callback callback) {
+    public void addCallback(Callback callback) {
+        assert(callback != null);
         mCallbacks.add(callback);
     }
 
+    public void addItemCallback(ItemCallback itemCallback) {
+        assert(itemCallback != null);
+        mItemCallbacks.add(itemCallback);
+    }
+
     public boolean hasSelection() {
         return !mSelection.isEmpty();
     }
@@ -172,12 +186,13 @@
     }
 
     /**
-     * Returns an unordered array of selected positions, including any
-     * provisional selection currently in effect.
+     * Restores the selected state of specified items. Used in cases such as restore the selection
+     * after rotation etc.
      */
     public void restoreSelection(Selection other) {
-        setItemsSelected(other.mSelection, true);
+        setItemsSelectedQuietly(other.mSelection, true);
         // NOTE: We intentionally don't restore provisional selection. It's provisional.
+        notifySelectionRestored();
     }
 
     /**
@@ -189,15 +204,20 @@
      * @return
      */
     public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
+        final boolean changed = setItemsSelectedQuietly(ids, selected);
+        notifySelectionChanged();
+        return changed;
+    }
+
+    private boolean setItemsSelectedQuietly(Iterable<String> ids, boolean selected) {
         boolean changed = false;
         for (String id: ids) {
-            boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id);
+            final boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id);
             if (itemChanged) {
                 notifyItemStateChanged(id, selected);
             }
             changed |= itemChanged;
         }
-        notifySelectionChanged();
         return changed;
     }
 
@@ -239,12 +259,10 @@
     public void toggleSelection(String modelId) {
         assert(modelId != null);
 
-        boolean changed = false;
-        if (mSelection.contains(modelId)) {
-            changed = attemptDeselect(modelId);
-        } else {
-            changed = attemptSelect(modelId);
-        }
+        final boolean changed =
+                mSelection.contains(modelId)
+                ? attemptDeselect(modelId)
+                : attemptSelect(modelId);
 
         if (changed) {
             notifySelectionChanged();
@@ -345,7 +363,7 @@
      */
     private boolean attemptDeselect(String id) {
         assert(id != null);
-        if (notifyBeforeItemStateChange(id, false)) {
+        if (canSetState(id, false)) {
             mSelection.remove(id);
             notifyItemStateChanged(id, false);
             if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
@@ -362,7 +380,7 @@
      */
     private boolean attemptSelect(String id) {
         assert(id != null);
-        boolean canSelect = notifyBeforeItemStateChange(id, true);
+        boolean canSelect = canSetState(id, true);
         if (!canSelect) {
             return false;
         }
@@ -374,14 +392,8 @@
         return true;
     }
 
-    boolean notifyBeforeItemStateChange(String id, boolean nextState) {
-        int lastListener = mCallbacks.size() - 1;
-        for (int i = lastListener; i > -1; i--) {
-            if (!mCallbacks.get(i).onBeforeItemStateChange(id, nextState)) {
-                return false;
-            }
-        }
-        return true;
+    boolean canSetState(String id, boolean nextState) {
+        return mCanSetState.test(id, nextState);
     }
 
     /**
@@ -390,9 +402,9 @@
      */
     void notifyItemStateChanged(String id, boolean selected) {
         assert(id != null);
-        int lastListener = mCallbacks.size() - 1;
-        for (int i = lastListener; i > -1; i--) {
-            mCallbacks.get(i).onItemStateChanged(id, selected);
+        int lastListener = mItemCallbacks.size() - 1;
+        for (int i = lastListener; i >= 0; i--) {
+            mItemCallbacks.get(i).onItemStateChanged(id, selected);
         }
         mAdapter.onItemSelectionChanged(id);
     }
@@ -410,6 +422,13 @@
         }
     }
 
+    private void notifySelectionRestored() {
+        int lastListener = mCallbacks.size() - 1;
+        for (int i = lastListener; i > -1; i--) {
+            mCallbacks.get(i).onSelectionRestored();
+        }
+    }
+
     private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
         switch (type) {
             case RANGE_REGULAR:
@@ -432,7 +451,7 @@
             }
 
             if (selected) {
-                boolean canSelect = notifyBeforeItemStateChange(id, true);
+                boolean canSelect = canSetState(id, true);
                 if (canSelect) {
                     if (mSingleSelect && hasSelection()) {
                         clearSelectionQuietly();
@@ -453,7 +472,10 @@
                 continue;
             }
             if (selected) {
-                mSelection.mProvisionalSelection.add(id);
+                boolean canSelect = canSetState(id, true);
+                if (canSelect) {
+                    mSelection.mProvisionalSelection.add(id);
+                }
             } else {
                 mSelection.mProvisionalSelection.remove(id);
             }
@@ -834,29 +856,24 @@
         };
     }
 
+    public interface ItemCallback {
+        void onItemStateChanged(String id, boolean selected);
+    }
+
     public interface Callback {
         /**
-         * Called when an item is selected or unselected while in selection mode.
-         *
-         * @param position Adapter position of the item that was checked or unchecked
-         * @param selected <code>true</code> if the item is now selected, <code>false</code>
-         *                if the item is now unselected.
-         */
-        public void onItemStateChanged(String id, boolean selected);
-
-        /**
-         * Called prior to an item changing state. Callbacks can cancel
-         * the change at {@code position} by returning {@code false}.
-         *
-         * @param id Adapter position of the item that was checked or unchecked
-         * @param selected <code>true</code> if the item is to be selected, <code>false</code>
-         *                if the item is to be unselected.
-         */
-        public boolean onBeforeItemStateChange(String id, boolean selected);
-
-        /**
          * Called immediately after completion of any set of changes.
          */
-        public void onSelectionChanged();
+        void onSelectionChanged();
+
+        /**
+         * Called immediately after selection is restored.
+         */
+        void onSelectionRestored();
+    }
+
+    @FunctionalInterface
+    public interface SelectionPredicate {
+        boolean test(String id, boolean nextState);
     }
 }
diff --git a/src/com/android/documentsui/dirlist/SelectionMetadata.java b/src/com/android/documentsui/dirlist/SelectionMetadata.java
new file mode 100644
index 0000000..6f91749
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/SelectionMetadata.java
@@ -0,0 +1,108 @@
+/*
+ * 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.dirlist;
+
+import static com.android.documentsui.model.DocumentInfo.getCursorInt;
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
+
+import android.database.Cursor;
+import android.provider.DocumentsContract.Document;
+import android.util.Log;
+
+import com.android.documentsui.MenuManager;
+import com.android.documentsui.MimePredicate;
+
+import java.util.function.Function;
+
+/**
+ * A class that holds metadata
+ */
+class SelectionMetadata implements MenuManager.SelectionDetails, MultiSelectManager.ItemCallback {
+
+    private static final String TAG = "SelectionMetadata";
+
+    private final MultiSelectManager mSelectionMgr;
+    private final Function<String, Cursor> mDocFinder;
+
+    // Partial files are files that haven't been fully downloaded.
+    private int mPartialCount = 0;
+    private int mDirectoryCount = 0;
+    private int mWritableDirectoryCount = 0;
+    private int mNoDeleteCount = 0;
+    private int mNoRenameCount = 0;
+
+    SelectionMetadata(
+            MultiSelectManager selectionMgr, Function<String, Cursor> docFinder) {
+        mSelectionMgr = selectionMgr;
+        mDocFinder = docFinder;
+    }
+
+    @Override
+    public void onItemStateChanged(String modelId, boolean selected) {
+        final Cursor cursor = mDocFinder.apply(modelId);
+        if (cursor == null) {
+            Log.w(TAG, "Model returned null cursor for document: " + modelId
+                    + ". Ignoring state changed event.");
+            return;
+        }
+
+        final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        if (MimePredicate.isDirectoryType(mimeType)) {
+            mDirectoryCount += selected ? 1 : -1;
+        }
+
+        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        if ((docFlags & Document.FLAG_PARTIAL) != 0) {
+            mPartialCount += selected ? 1 : -1;
+        }
+        if ((docFlags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0) {
+            mWritableDirectoryCount += selected ? 1 : -1;
+        }
+        if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
+            mNoDeleteCount += selected ? 1 : -1;
+        }
+        if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) {
+            mNoRenameCount += selected ? 1 : -1;
+        }
+    }
+
+    @Override
+    public boolean containsDirectories() {
+        return mDirectoryCount > 0;
+    }
+
+    @Override
+    public boolean containsPartialFiles() {
+        return mPartialCount > 0;
+    }
+
+    @Override
+    public boolean canDelete() {
+        return mNoDeleteCount == 0;
+    }
+
+    @Override
+    public boolean canRename() {
+        return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1;
+    }
+
+    @Override
+    public boolean canPasteInto() {
+        return mDirectoryCount == 1 && mWritableDirectoryCount == 1
+                && mSelectionMgr.getSelection().size() == 1;
+    }
+}
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
index bf6ac3c..604ccd6 100644
--- a/src/com/android/documentsui/dirlist/UserInputHandler.java
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -25,8 +25,8 @@
 import android.view.MotionEvent;
 
 import com.android.documentsui.Events;
-import com.android.documentsui.Events.EventHandler;
 import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.base.FunctionalInterfaces.EventHandler;
 
 import java.util.Collections;
 import java.util.function.Function;
@@ -47,11 +47,11 @@
     private final FocusHandler mFocusHandler;
     private final Function<MotionEvent, T> mEventConverter;
     private final Predicate<DocumentDetails> mSelectable;
-    private final EventHandler mRightClickHandler;
+    private final EventHandler<InputEvent> mRightClickHandler;
     private final DocumentHandler mActivateHandler;
     private final DocumentHandler mDeleteHandler;
-    private final EventHandler mTouchDragListener;
-    private final EventHandler mGestureSelectHandler;
+    private final EventHandler<InputEvent> mTouchDragListener;
+    private final EventHandler<InputEvent> mGestureSelectHandler;
     private final TouchInputDelegate mTouchDelegate;
     private final MouseInputDelegate mMouseDelegate;
     private final KeyInputHandler mKeyListener;
@@ -61,11 +61,11 @@
             FocusHandler focusHandler,
             Function<MotionEvent, T> eventConverter,
             Predicate<DocumentDetails> selectable,
-            EventHandler rightClickHandler,
+            EventHandler<InputEvent> rightClickHandler,
             DocumentHandler activateHandler,
             DocumentHandler deleteHandler,
-            EventHandler touchDragListener,
-            EventHandler gestureSelectHandler) {
+            EventHandler<InputEvent> touchDragListener,
+            EventHandler<InputEvent> gestureSelectHandler) {
 
         mSelectionMgr = selectionMgr;
         mFocusHandler = focusHandler;
diff --git a/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java
index d51ef1f..81612a9 100644
--- a/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java
+++ b/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java
@@ -26,6 +26,7 @@
 import com.android.documentsui.State;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.testing.TestEvent;
+import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.Views;
 
 import java.util.ArrayList;
@@ -41,10 +42,7 @@
 
     @Override
     public void setUp() throws Exception {
-
-        mMultiSelectManager = new MultiSelectManager(
-                new TestDocumentsAdapter(new ArrayList<String>()),
-                MultiSelectManager.MODE_MULTIPLE);
+        mMultiSelectManager = MultiSelectManagers.createTestInstance();
 
         mListener = new DragStartListener.ActiveListener(
                 new State(),
diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 237899b..1c7b863 100644
--- a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -16,38 +16,49 @@
 
 package com.android.documentsui.dirlist;
 
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
 import android.util.SparseBooleanArray;
 
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 import com.android.documentsui.testing.dirlist.TestSelectionListener;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+@RunWith(AndroidJUnit4.class)
 @SmallTest
-public class MultiSelectManagerTest extends AndroidTestCase {
+public class MultiSelectManagerTest {
 
     private static final List<String> ITEMS = TestData.create(100);
 
+    private final Set<String> mIgnored = new HashSet<>();
     private MultiSelectManager mManager;
     private TestSelectionListener mCallback;
-    private TestDocumentsAdapter mAdapter;
     private SelectionProbe mSelection;
 
-    @Override
+    @Before
     public void setUp() throws Exception {
         mCallback = new TestSelectionListener();
-        mAdapter = new TestDocumentsAdapter(ITEMS);
-        mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+        mManager = MultiSelectManagers.createTestInstance(
+                ITEMS,
+                MultiSelectManager.MODE_MULTIPLE,
+                (String id, boolean nextState) -> (!nextState || !mIgnored.contains(id)));
         mManager.addCallback(mCallback);
 
         mSelection = new SelectionProbe(mManager);
+
+        mIgnored.clear();
     }
 
+    @Test
     public void testSelection() {
         // Check selection.
         mManager.toggleSelection(ITEMS.get(7));
@@ -57,6 +68,15 @@
         mSelection.assertNoSelection();
     }
 
+    @Test
+    public void testSelection_DoNothingOnUnselectableItem() {
+        mIgnored.add(ITEMS.get(7));
+
+        mManager.toggleSelection(ITEMS.get(7));
+        mSelection.assertNoSelection();
+    }
+
+    @Test
     public void testSelection_NotifiesSelectionChanged() {
         // Selection should notify.
         mManager.toggleSelection(ITEMS.get(7));
@@ -66,12 +86,26 @@
         mCallback.assertSelectionChanged();
     }
 
+    @Test
     public void testRangeSelection() {
         mManager.startRangeSelection(15);
         mManager.snapRangeSelection(19);
         mSelection.assertRangeSelection(15, 19);
     }
 
+    @Test
+    public void testRangeSelection_SkipUnselectableItem() {
+        mIgnored.add(ITEMS.get(17));
+
+        mManager.startRangeSelection(15);
+        mManager.snapRangeSelection(19);
+
+        mSelection.assertRangeSelected(15, 16);
+        mSelection.assertNotSelected(17);
+        mSelection.assertRangeSelected(18, 19);
+    }
+
+    @Test
     public void testRangeSelection_snapExpand() {
         mManager.startRangeSelection(15);
         mManager.snapRangeSelection(19);
@@ -79,6 +113,7 @@
         mSelection.assertRangeSelection(15, 27);
     }
 
+    @Test
     public void testRangeSelection_snapContract() {
         mManager.startRangeSelection(15);
         mManager.snapRangeSelection(27);
@@ -86,6 +121,7 @@
         mSelection.assertRangeSelection(15, 19);
     }
 
+    @Test
     public void testRangeSelection_snapInvert() {
         mManager.startRangeSelection(15);
         mManager.snapRangeSelection(27);
@@ -93,6 +129,7 @@
         mSelection.assertRangeSelection(3, 15);
     }
 
+    @Test
     public void testRangeSelection_multiple() {
         mManager.startRangeSelection(15);
         mManager.snapRangeSelection(27);
@@ -104,6 +141,7 @@
         mSelection.assertRangeSelected(42, 57);
     }
 
+    @Test
     public void testProvisionalRangeSelection() {
         mManager.startRangeSelection(13);
         mManager.snapProvisionalRangeSelection(15);
@@ -113,6 +151,7 @@
         mSelection.assertSelectionSize(3);
     }
 
+    @Test
     public void testProvisionalRangeSelection_endEarly() {
         mManager.startRangeSelection(13);
         mManager.snapProvisionalRangeSelection(15);
@@ -124,6 +163,7 @@
         mSelection.assertSelectionSize(1);
     }
 
+    @Test
     public void testProvisionalRangeSelection_snapExpand() {
         mManager.startRangeSelection(13);
         mManager.snapProvisionalRangeSelection(15);
@@ -133,6 +173,7 @@
         mSelection.assertRangeSelection(13, 18);
     }
 
+    @Test
     public void testCombinationRangeSelection_IntersectsOldSelection() {
         mManager.startRangeSelection(13);
         mManager.snapRangeSelection(15);
@@ -148,6 +189,7 @@
         mSelection.assertSelectionSize(4);
     }
 
+    @Test
     public void testProvisionalSelection() {
         Selection s = mManager.getSelection();
         mSelection.assertNoSelection();
@@ -159,6 +201,7 @@
         mSelection.assertSelection(1, 2);
     }
 
+    @Test
     public void testProvisionalSelection_Replace() {
         Selection s = mManager.getSelection();
 
@@ -174,6 +217,7 @@
         mSelection.assertSelection(3, 4);
     }
 
+    @Test
     public void testProvisionalSelection_IntersectsExistingProvisionalSelection() {
         Selection s = mManager.getSelection();
 
@@ -188,6 +232,7 @@
         mSelection.assertSelection(1);
     }
 
+    @Test
     public void testProvisionalSelection_Apply() {
         Selection s = mManager.getSelection();
 
@@ -199,6 +244,7 @@
         mSelection.assertSelection(1, 2);
     }
 
+    @Test
     public void testProvisionalSelection_Cancel() {
         mManager.toggleSelection(ITEMS.get(1));
         mManager.toggleSelection(ITEMS.get(2));
@@ -214,6 +260,7 @@
         mSelection.assertSelection(1, 2);
     }
 
+    @Test
     public void testProvisionalSelection_IntersectsAppliedSelection() {
         mManager.toggleSelection(ITEMS.get(1));
         mManager.toggleSelection(ITEMS.get(2));
diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java
index 444b2dc..11818ac 100644
--- a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java
+++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java
@@ -16,17 +16,27 @@
 
 package com.android.documentsui.dirlist;
 
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+
 import com.google.common.collect.Sets;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.HashSet;
 import java.util.Set;
 
+@RunWith(AndroidJUnit4.class)
 @SmallTest
-public class MultiSelectManager_SelectionTest extends AndroidTestCase {
+public class MultiSelectManager_SelectionTest {
 
     private Selection selection;
 
@@ -36,7 +46,7 @@
             "auth|id=@53di*/f3#d"
     };
 
-    @Override
+    @Before
     public void setUp() throws Exception {
         selection = new Selection();
         selection.add(ids[0]);
@@ -44,6 +54,7 @@
         selection.add(ids[2]);
     }
 
+    @Test
     public void testAdd() {
         // We added in setUp.
         assertEquals(3, selection.size());
@@ -52,6 +63,7 @@
         assertContains(ids[2]);
     }
 
+    @Test
     public void testRemove() {
         selection.remove(ids[0]);
         selection.remove(ids[2]);
@@ -59,11 +71,13 @@
         assertContains(ids[1]);
     }
 
+    @Test
     public void testClear() {
         selection.clear();
         assertEquals(0, selection.size());
     }
 
+    @Test
     public void testIsEmpty() {
         assertTrue(new Selection().isEmpty());
         selection.clear();
diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java
index 62cb1b0..020f316 100644
--- a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java
+++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java
@@ -16,16 +16,24 @@
 
 package com.android.documentsui.dirlist;
 
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
+import static junit.framework.Assert.fail;
 
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.dirlist.SelectionProbe;
 import com.android.documentsui.testing.dirlist.TestSelectionListener;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.List;
 
+@RunWith(AndroidJUnit4.class)
 @SmallTest
-public class MultiSelectManager_SingleSelectTest extends AndroidTestCase {
+public class MultiSelectManager_SingleSelectTest {
 
     private static final List<String> ITEMS = TestData.create(100);
 
@@ -34,16 +42,16 @@
     private TestDocumentsAdapter mAdapter;
     private SelectionProbe mSelection;
 
-    @Override
+    @Before
     public void setUp() throws Exception {
         mCallback = new TestSelectionListener();
-        mAdapter = new TestDocumentsAdapter(ITEMS);
-        mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE);
+        mManager = MultiSelectManagers.createTestInstance(ITEMS, MultiSelectManager.MODE_SINGLE);
         mManager.addCallback(mCallback);
 
         mSelection = new SelectionProbe(mManager);
     }
 
+    @Test
     public void testSimpleSelect() {
         mManager.toggleSelection(ITEMS.get(3));
         mManager.toggleSelection(ITEMS.get(4));
@@ -51,6 +59,7 @@
         mSelection.assertSelection(4);
     }
 
+    @Test
     public void testRangeSelectionNotEstablished() {
         mManager.toggleSelection(ITEMS.get(3));
         mCallback.reset();
diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
index ef8fd9f..0d99bab 100644
--- a/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
+++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -24,6 +24,7 @@
 import android.view.MotionEvent;
 
 import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
 import com.android.documentsui.testing.TestPredicate;
@@ -57,9 +58,7 @@
     @Before
     public void setUp() {
 
-        mAdapter = new TestDocumentsAdapter(ITEMS);
-        MultiSelectManager selectionMgr =
-                new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+        MultiSelectManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
 
         mSelection = new SelectionProbe(selectionMgr);
         mCanSelect = new TestPredicate<>();
diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
index 1223e3a..6500a56 100644
--- a/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
+++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java
@@ -21,6 +21,7 @@
 import android.view.MotionEvent;
 
 import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
 import com.android.documentsui.testing.TestPredicate;
@@ -57,9 +58,7 @@
     @Before
     public void setUp() {
 
-        mAdapter = new TestDocumentsAdapter(ITEMS);
-        MultiSelectManager selectionMgr =
-                new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+        MultiSelectManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
 
         mSelection = new SelectionProbe(selectionMgr);
         mCanSelect = new TestPredicate<>();
diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
index 11c4222..3946fd6 100644
--- a/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
+++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java
@@ -24,6 +24,7 @@
 import android.view.MotionEvent;
 
 import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.testing.MultiSelectManagers;
 import com.android.documentsui.testing.TestEvent;
 import com.android.documentsui.testing.TestEvent.Builder;
 import com.android.documentsui.testing.TestPredicate;
@@ -56,10 +57,7 @@
 
     @Before
     public void setUp() {
-
-        mAdapter = new TestDocumentsAdapter(ITEMS);
-        MultiSelectManager selectionMgr =
-                new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+        MultiSelectManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS);
 
         mSelection = new SelectionProbe(selectionMgr);
         mCanSelect = new TestPredicate<>();
diff --git a/tests/src/com/android/documentsui/testing/MultiSelectManagers.java b/tests/src/com/android/documentsui/testing/MultiSelectManagers.java
new file mode 100644
index 0000000..e3b4261
--- /dev/null
+++ b/tests/src/com/android/documentsui/testing/MultiSelectManagers.java
@@ -0,0 +1,56 @@
+/*
+ * 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.dirlist.MultiSelectManager;
+import com.android.documentsui.dirlist.MultiSelectManager.SelectionMode;
+import com.android.documentsui.dirlist.MultiSelectManager.SelectionPredicate;
+import com.android.documentsui.dirlist.TestDocumentsAdapter;
+
+import java.util.Collections;
+import java.util.List;
+
+public class MultiSelectManagers {
+    private MultiSelectManagers() {}
+
+    public static MultiSelectManager createTestInstance() {
+        return createTestInstance(Collections.emptyList());
+    }
+
+    public static MultiSelectManager createTestInstance(List<String> docs) {
+        return createTestInstance(docs, MultiSelectManager.MODE_MULTIPLE);
+    }
+
+    public static MultiSelectManager createTestInstance(
+            List<String> docs, @SelectionMode int mode) {
+        return createTestInstance(
+                docs,
+                mode,
+                (String id, boolean nextState) -> true);
+    }
+
+    public static MultiSelectManager createTestInstance(
+            List<String> docs, @SelectionMode int mode, SelectionPredicate canSetState) {
+        TestDocumentsAdapter adapter = new TestDocumentsAdapter(docs);
+        MultiSelectManager manager = new MultiSelectManager(
+                adapter,
+                mode,
+                canSetState);
+
+        return manager;
+    }
+}
diff --git a/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java b/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java
index 08f29f0..06c2219 100644
--- a/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java
+++ b/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java
@@ -21,27 +21,18 @@
 
 import com.android.documentsui.dirlist.MultiSelectManager;
 
-import java.util.HashSet;
-import java.util.Set;
-
 public final class TestSelectionListener implements MultiSelectManager.Callback {
 
-    Set<String> ignored = new HashSet<>();
     private boolean mSelectionChanged = false;
 
     @Override
-    public void onItemStateChanged(String modelId, boolean selected) {}
-
-    @Override
-    public boolean onBeforeItemStateChange(String modelId, boolean selected) {
-        return !ignored.contains(modelId);
-    }
-
-    @Override
     public void onSelectionChanged() {
         mSelectionChanged = true;
     }
 
+    @Override
+    public void onSelectionRestored() {}
+
     public void reset() {
         mSelectionChanged = false;
     }