Refactor SelectionModeListener.

* Move ActionMode logic into a new class ActionModeController
* Move SelectionDetails logic into MultiSelectionController
* Merge canSelect() and onBeforeItemStateChange()
* Add some basic unit tests for selecting unselectable items
* Fix a bug that selects unselectable items using gestural selection
* Convert MultiSelectManagerTests to JUnit4

Change-Id: I14642178ff39e7b990cc9f3fb0d9f40e6309e087
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);
+    }
+}