Integrate band selection into the files app.

This consists of:
1) Moving both BandSelect* classes into MultiSelectManager as nested
classes. This removes extra layers of indirection and makes code easier
to comprehend.
2) Fixing an issue where band selection could start within an item
instead of only within margins between items.
3) Adding "provisional selection" support to the Selection class. This
gives band select the ability to select some items "temporarily" such
that when the band select no longer covers those items, they are
deselected.
4) Adding glue code between the classes.

Bug: 23081429, 23083146, 20669231

Change-Id: I0dc57e2c0d2ccedb3e1218f0e496de637be227a2
diff --git a/tests/src/com/android/documentsui/BandSelectModelTest.java b/tests/src/com/android/documentsui/BandSelectModelTest.java
new file mode 100644
index 0000000..20c4548
--- /dev/null
+++ b/tests/src/com/android/documentsui/BandSelectModelTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import static org.junit.Assert.*;
+
+import com.android.documentsui.MultiSelectManager.BandSelectModel;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.util.SparseBooleanArray;
+
+import org.junit.After;
+import org.junit.Test;
+
+public class BandSelectModelTest {
+
+    private static final int VIEW_PADDING_PX = 5;
+    private static final int CHILD_VIEW_EDGE_PX = 100;
+    private static final int VIEWPORT_HEIGHT = 500;
+
+    private static BandSelectModel model;
+    private static TestHelper helper;
+    private static SparseBooleanArray lastSelection;
+    private static int viewWidth;
+
+    private static void setUp(int numChildren, int numColumns) {
+        helper = new TestHelper(numChildren, numColumns);
+        viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
+        model = new BandSelectModel(helper);
+        model.addOnSelectionChangedListener(new BandSelectModel.OnSelectionChangedListener() {
+
+            @Override
+            public void onSelectionChanged(SparseBooleanArray updatedSelection) {
+                lastSelection = updatedSelection;
+            }
+        });
+    }
+
+    @After
+    public void tearDown() {
+        model = null;
+        helper = null;
+        lastSelection = null;
+    }
+
+    @Test
+    public void testSelectionLeftOfItems() {
+        setUp(20, 5);
+        model.startSelection(new Point(0, 10));
+        model.resizeSelection(new Point(1, 11));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionRightOfItems() {
+        setUp(20, 4);
+        model.startSelection(new Point(viewWidth - 1, 10));
+        model.resizeSelection(new Point(viewWidth - 2, 11));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionAboveItems() {
+        setUp(20, 4);
+        model.startSelection(new Point(10, 0));
+        model.resizeSelection(new Point(11, 1));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionBelowItems() {
+        setUp(5, 4);
+        model.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
+        model.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testVerticalSelectionBetweenItems() {
+        setUp(20, 4);
+        model.startSelection(new Point(106, 0));
+        model.resizeSelection(new Point(107, 200));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testHorizontalSelectionBetweenItems() {
+        setUp(20, 4);
+        model.startSelection(new Point(0, 105));
+        model.resizeSelection(new Point(200, 106));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testGrowingAndShrinkingSelection() {
+        setUp(20, 4);
+        model.startSelection(new Point(0, 0));
+        model.resizeSelection(new Point(5, 5));
+        assertSelected(new int[] {0});
+        model.resizeSelection(new Point(109, 109));
+        assertSelected(new int[] {0});
+        model.resizeSelection(new Point(110, 109));
+        assertSelected(new int[] {0, 1});
+        model.resizeSelection(new Point(110, 110));
+        assertSelected(new int[] {0, 1, 4, 5});
+        model.resizeSelection(new Point(214, 214));
+        assertSelected(new int[] {0, 1, 4, 5});
+        model.resizeSelection(new Point(215, 214));
+        assertSelected(new int[] {0, 1, 2, 4, 5, 6});
+        model.resizeSelection(new Point(214, 214));
+        assertSelected(new int[] {0, 1, 4, 5});
+        model.resizeSelection(new Point(110, 110));
+        assertSelected(new int[] {0, 1, 4, 5});
+        model.resizeSelection(new Point(110, 109));
+        assertSelected(new int[] {0, 1});
+        model.resizeSelection(new Point(109, 109));
+        assertSelected(new int[] {0});
+        model.resizeSelection(new Point(5, 5));
+        assertSelected(new int[] {0});
+        model.resizeSelection(new Point(0, 0));
+        assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionMovingAroundOrigin() {
+        setUp(16, 4);
+        model.startSelection(new Point(210, 210));
+        model.resizeSelection(new Point(viewWidth - 1, 0));
+        assertSelected(new int[] {2, 3, 6, 7});
+        model.resizeSelection(new Point(0, 0));
+        assertSelected(new int[] {0, 1, 4, 5});
+        model.resizeSelection(new Point(0, 420));
+        assertSelected(new int[] {8, 9, 12, 13});
+        model.resizeSelection(new Point(viewWidth - 1, 420));
+        assertSelected(new int[] {10, 11, 14, 15});
+        assertEquals(10, model.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testScrollingBandSelect() {
+        setUp(40, 4);
+        model.startSelection(new Point(0, 0));
+        model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+        assertSelected(new int[] {0, 4, 8, 12, 16});
+        scroll(CHILD_VIEW_EDGE_PX);
+        assertSelected(new int[] {0, 4, 8, 12, 16, 20});
+        model.resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
+        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21});
+        scroll(CHILD_VIEW_EDGE_PX);
+        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21, 24, 25});
+        scroll(-2 * CHILD_VIEW_EDGE_PX);
+        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17});
+        model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+        assertSelected(new int[] {0, 4, 8, 12, 16});
+        assertEquals(0, model.getPositionNearestOrigin());
+    }
+
+    private static void assertSelected(int[] selectedPositions) {
+        assertEquals(selectedPositions.length, lastSelection.size());
+        for (int position : selectedPositions) {
+            assertTrue(lastSelection.get(position));
+        }
+    }
+
+    private static void scroll(int dy) {
+        assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight());
+        helper.verticalOffset += dy;
+        model.onScrolled(null, 0, dy);
+    }
+
+    private static final class TestHelper implements MultiSelectManager.BandModelHelper {
+
+        public int horizontalOffset = 0;
+        public int verticalOffset = 0;
+        private final int mNumColumns;
+        private final int mNumRows;
+        private final int mNumChildren;
+
+        public TestHelper(int numChildren, int numColumns) {
+            mNumChildren = numChildren;
+            mNumColumns = numColumns;
+            mNumRows = (int) Math.ceil((double) numChildren / mNumColumns);
+        }
+
+        private int getTotalHeight() {
+            return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1);
+        }
+
+        private int getFirstVisibleRowIndex() {
+            return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+        }
+
+        private int getLastVisibleRowIndex() {
+            int lastVisibleRowUncapped =
+                    (VIEWPORT_HEIGHT + verticalOffset - 1) / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+            return Math.min(lastVisibleRowUncapped, mNumRows - 1);
+        }
+
+        private int getNumItemsInRow(int index) {
+            assertTrue(index >= 0 && index < mNumRows);
+            if (index == mNumRows - 1 && mNumChildren % mNumColumns != 0) {
+                return mNumChildren % mNumColumns;
+            }
+
+            return mNumColumns;
+        }
+
+        @Override
+        public void addOnScrollListener(OnScrollListener listener) {}
+
+        @Override
+        public void removeOnScrollListener(OnScrollListener listener) {}
+
+        @Override
+        public Point createAbsolutePoint(Point relativePoint) {
+            return new Point(
+                    relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset);
+        }
+
+        @Override
+        public int getVisibleChildCount() {
+            int childCount = 0;
+            for (int i = getFirstVisibleRowIndex(); i <= getLastVisibleRowIndex(); i++) {
+                childCount += getNumItemsInRow(i);
+            }
+            return childCount;
+        }
+
+        @Override
+        public int getAdapterPositionAt(int index) {
+            return index + mNumColumns * (getFirstVisibleRowIndex());
+        }
+
+        @Override
+        public Rect getAbsoluteRectForChildViewAt(int index) {
+            int adapterPosition = getAdapterPositionAt(index);
+            int rowIndex = adapterPosition / mNumColumns;
+            int columnIndex = adapterPosition % mNumColumns;
+
+            Rect rect = new Rect();
+            rect.top = VIEW_PADDING_PX + rowIndex * (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+            rect.bottom = rect.top + CHILD_VIEW_EDGE_PX - 1;
+            rect.left = VIEW_PADDING_PX + columnIndex * (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+            rect.right = rect.left + CHILD_VIEW_EDGE_PX - 1;
+            return rect;
+        }
+
+        @Override
+        public int getTotalChildCount() {
+            return mNumChildren;
+        }
+
+        @Override
+        public int getNumColumns() {
+            return mNumColumns;
+        }
+
+        @Override
+        public int getNumRows() {
+            return mNumRows;
+        }
+    }
+}