OSDN Git Service

Integrate band selection into the files app.
authorKyle Horimoto <khorimoto@google.com>
Tue, 18 Aug 2015 20:25:29 +0000 (13:25 -0700)
committerKyle Horimoto <khorimoto@google.com>
Thu, 27 Aug 2015 21:43:30 +0000 (14:43 -0700)
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: 230814292308314620669231

Change-Id: I0dc57e2c0d2ccedb3e1218f0e496de637be227a2

packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java [deleted file]
packages/DocumentsUI/src/com/android/documentsui/BandSelectMatrix.java [deleted file]
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
packages/DocumentsUI/src/com/android/documentsui/MultiSelectManager.java
packages/DocumentsUI/tests/src/com/android/documentsui/BandSelectModelTest.java [moved from packages/DocumentsUI/tests/src/com/android/documentsui/BandSelectMatrixTest.java with 71% similarity]
packages/DocumentsUI/tests/src/com/android/documentsui/MultiSelectManagerTest.java
packages/DocumentsUI/tests/src/com/android/documentsui/UnitTests.java

diff --git a/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java
deleted file mode 100644 (file)
index 74170f5..0000000
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * 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 com.android.documentsui.Events.isMouseEvent;
-import static com.android.internal.util.Preconditions.checkState;
-
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.ViewHolder;
-import android.util.Log;
-import android.util.SparseBooleanArray;
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * Provides mouse driven band-select support when used in conjuction with {@link RecyclerView} and
- * {@link MultiSelectManager}. This class is responsible for rendering the band select overlay and
- * selecting overlaid items via MultiSelectManager.
- */
-public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener {
-
-    private static final int NOT_SELECTED = -1;
-    private static final int NOT_SET = -1;
-
-    // For debugging purposes.
-    private static final String TAG = "BandSelectManager";
-    private static final boolean DEBUG = false;
-
-    private final RecyclerView mRecyclerView;
-    private final MultiSelectManager mSelectManager;
-    private final Drawable mRegionSelectorDrawable;
-    private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray();
-
-    private boolean mIsBandSelectActive = false;
-    private Point mOrigin;
-    private Point mPointer;
-    private Rect mBounds;
-
-    // Maintain the last selection made by band, so if bounds shrink back, we can deselect
-    // the respective items.
-    private int mCursorDeltaY = 0;
-    private int mFirstSelected = NOT_SELECTED;
-
-    // The time at which the current band selection-induced scroll began. If no scroll is in
-    // progress, the value is NOT_SET.
-    private long mScrollStartTime = NOT_SET;
-    private final Runnable mScrollRunnable = new Runnable() {
-        /**
-         * The number of milliseconds of scrolling at which scroll speed continues to increase. At
-         * first, the scroll starts slowly; then, the rate of scrolling increases until it reaches
-         * its maximum value at after this many milliseconds.
-         */
-        private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
-
-        @Override
-        public void run() {
-            // Compute the number of pixels the pointer's y-coordinate is past the view. Negative
-            // values mean the pointer is at or before the top of the view, and positive values mean
-            // that the pointer is at or after the bottom of the view. Note that one additional
-            // pixel is added here so that the view still scrolls when the pointer is exactly at the
-            // top or bottom.
-            int pixelsPastView = 0;
-            if (mPointer.y <= 0) {
-                pixelsPastView = mPointer.y - 1;
-            } else if (mPointer.y >= mRecyclerView.getHeight() - 1) {
-                pixelsPastView = mPointer.y - mRecyclerView.getHeight() + 1;
-            }
-
-            if (!mIsBandSelectActive || pixelsPastView == 0) {
-                // If band selection is inactive, or if it is active but not at the edge of the
-                // view, no scrolling is necessary.
-                mScrollStartTime = NOT_SET;
-                return;
-            }
-
-            if (mScrollStartTime == NOT_SET) {
-                // If the pointer was previously not at the edge of the view but now is, set the
-                // start time for the scroll.
-                mScrollStartTime = System.currentTimeMillis();
-            }
-
-            // Compute the number of pixels to scroll, and scroll that many pixels.
-            final int numPixels = computeNumPixelsToScroll(
-                    pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
-            mRecyclerView.scrollBy(0, numPixels);
-
-            // Adjust the y-coordinate of the origin the opposite number of pixels so that the
-            // origin remains in the same place relative to the view's items.
-            mOrigin.y -= numPixels;
-            resizeBandSelectRectangle();
-
-            mRecyclerView.removeCallbacks(mScrollRunnable);
-            mRecyclerView.postOnAnimation(this);
-        }
-
-        /**
-         * Computes the number of pixels to scroll based on how far the pointer is past the end of
-         * the view and how long it has been there. Roughly based on ItemTouchHelper's algorithm for
-         * computing the number of pixels to scroll when an item is dragged to the end of a
-         * {@link RecyclerView}.
-         * @param pixelsPastView
-         * @param scrollDuration
-         * @return
-         */
-        private int computeNumPixelsToScroll(int pixelsPastView, long scrollDuration) {
-            final int maxScrollStep = computeMaxScrollStep(mRecyclerView);
-            final int direction = (int) Math.signum(pixelsPastView);
-            final int absPastView = Math.abs(pixelsPastView);
-
-            // Calculate the ratio of how far out of the view the pointer currently resides to the
-            // entire height of the view.
-            final float outOfBoundsRatio = Math.min(
-                    1.0f, (float) absPastView / mRecyclerView.getHeight());
-            // Interpolate this ratio and use it to compute the maximum scroll that should be
-            // possible for this step.
-            final float cappedScrollStep =
-                    direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
-
-            // Likewise, calculate the ratio of the time spent in the scroll to the limit.
-            final float timeRatio = Math.min(
-                    1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
-            // Interpolate this ratio and use it to compute the final number of pixels to scroll.
-            final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
-
-            // If the final number of pixels to scroll ends up being 0, the view should still scroll
-            // at least one pixel.
-            return numPixels != 0 ? numPixels : direction;
-        }
-
-        /**
-         * Computes the maximum scroll allowed for a given animation frame. Currently, this
-         * defaults to the height of the view, but this could be tweaked if this results in scrolls
-         * that are too fast or too slow.
-         * @param rv
-         * @return
-         */
-        private int computeMaxScrollStep(RecyclerView rv) {
-            return rv.getHeight();
-        }
-
-        /**
-         * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends at
-         * (1,1) and quickly approaches 1 near the start of that interval. This ensures that drags
-         * that are at the edge or barely past the edge of the view still cause sufficient
-         * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if needed.
-         * @param ratio A ratio which is in the range [0, 1].
-         * @return A "smoothed" value, also in the range [0, 1].
-         */
-        private float smoothOutOfBoundsRatio(float ratio) {
-            return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
-        }
-
-        /**
-         * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1) and
-         * stays close to 0 for most input values except those very close to 1. This ensures that
-         * scrolls start out very slowly but speed up drastically after the scroll has been in
-         * progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used, but this
-         * could also be tweaked if needed.
-         * @param ratio A ratio which is in the range [0, 1].
-         * @return A "smoothed" value, also in the range [0, 1].
-         */
-        private float smoothTimeRatio(float ratio) {
-            return (float) Math.pow(ratio, 5);
-        }
-    };
-
-    /**
-     * @param recyclerView
-     * @param multiSelectManager
-     */
-    public BandSelectManager(RecyclerView recyclerView, MultiSelectManager multiSelectManager) {
-        mRecyclerView = recyclerView;
-        mSelectManager = multiSelectManager;
-        mRegionSelectorDrawable =
-            mRecyclerView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
-
-        mRecyclerView.addOnItemTouchListener(this);
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
-        // Only intercept the event if it was triggered by a mouse. If band select is inactive,
-        // do not intercept ACTION_UP events as they will not be processed.
-        return isMouseEvent(e) &&
-                (mIsBandSelectActive || e.getActionMasked() != MotionEvent.ACTION_UP);
-    }
-
-    @Override
-    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
-        checkState(isMouseEvent(e));
-        processMotionEvent(e);
-    }
-
-    /**
-     * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
-     * @param e
-     */
-    private void processMotionEvent(MotionEvent e) {
-        if (mIsBandSelectActive && e.getActionMasked() == MotionEvent.ACTION_UP) {
-            endBandSelect();
-            return;
-        }
-
-        mPointer = new Point((int) e.getX(), (int) e.getY());
-        if (!mIsBandSelectActive) {
-            startBandSelect();
-        }
-
-        scrollViewIfNecessary();
-        resizeBandSelectRectangle();
-        selectChildrenCoveredBySelection();
-    }
-
-    /**
-     * Starts band select by adding the drawable to the RecyclerView's overlay.
-     */
-    private void startBandSelect() {
-        if (DEBUG) Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");
-        mIsBandSelectActive = true;
-        mOrigin = mPointer;
-        mRecyclerView.getOverlay().add(mRegionSelectorDrawable);
-    }
-
-    /**
-     * Scrolls the view if necessary.
-     */
-    private void scrollViewIfNecessary() {
-        mRecyclerView.removeCallbacks(mScrollRunnable);
-        mScrollRunnable.run();
-        mRecyclerView.invalidate();
-    }
-
-    /**
-     * Resizes the band select rectangle by using the origin and the current pointer positoin as
-     * two opposite corners of the selection.
-     */
-    private void resizeBandSelectRectangle() {
-        if (mBounds != null) {
-            mCursorDeltaY = mPointer.y - mBounds.bottom;
-        }
-
-        mBounds = new Rect(Math.min(mOrigin.x, mPointer.x),
-                Math.min(mOrigin.y, mPointer.y),
-                Math.max(mOrigin.x, mPointer.x),
-                Math.max(mOrigin.y, mPointer.y));
-
-        mRegionSelectorDrawable.setBounds(mBounds);
-    }
-
-    /**
-     * Selects the children covered by the band select overlay by delegating to MultiSelectManager.
-     * TODO: Provide a finished implementation. This is down and dirty, proof of concept code.
-     * Final optimized implementation, with support for managing offscreen selection to come.
-     */
-    private void selectChildrenCoveredBySelection() {
-
-        // track top and bottom selections. Details on why this is useful below.
-        int first = NOT_SELECTED;
-        int last = NOT_SELECTED;
-
-        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
-
-            View child = mRecyclerView.getChildAt(i);
-            ViewHolder holder = mRecyclerView.getChildViewHolder(child);
-            Rect childRect = new Rect();
-            child.getHitRect(childRect);
-
-            boolean shouldSelect = Rect.intersects(childRect, mBounds);
-            int position = holder.getAdapterPosition();
-
-            // This also allows us to clear the selection of elements
-            // that only temporarily entered the bounds of the band.
-            if (mSelectedByBand.get(position) && !shouldSelect) {
-                mSelectManager.setItemSelected(position, false);
-                mSelectedByBand.delete(position);
-            }
-
-            // We need to keep track of the first and last items selected.
-            // We'll use this information along with cursor direction
-            // to determine the starting point of the selection.
-            // We provide this information to selection manager
-            // to enable more natural user interaction when working
-            // with Shift+Click and multiple contiguous selection ranges.
-            if (shouldSelect) {
-                if (first == NOT_SELECTED) {
-                    first = position;
-                } else {
-                    last = position;
-                }
-                mSelectManager.setItemSelected(position, true);
-                mSelectedByBand.put(position, true);
-            }
-        }
-
-        // Remember which is the last selected item, so we can
-        // share that with selection manager when band select ends.
-        // It'll use that as it's begin selection point when
-        // user SHIFT+Clicks.
-        if (mCursorDeltaY < 0 && last != NOT_SELECTED) {
-            mFirstSelected = last;
-        } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) {
-            mFirstSelected = first;
-        }
-    }
-
-    /**
-     * Ends band select by removing the overlay.
-     */
-    private void endBandSelect() {
-        if (DEBUG) Log.d(TAG, "Ending band select.");
-        mIsBandSelectActive = false;
-        mSelectedByBand.clear();
-        mRecyclerView.getOverlay().remove(mRegionSelectorDrawable);
-        if (mFirstSelected != NOT_SELECTED) {
-            mSelectManager.setSelectionFocusBegin(mFirstSelected);
-        }
-    }
-}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BandSelectMatrix.java b/packages/DocumentsUI/src/com/android/documentsui/BandSelectMatrix.java
deleted file mode 100644 (file)
index f259059..0000000
+++ /dev/null
@@ -1,645 +0,0 @@
-/*
- * Copyright (C) 2013 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 java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import com.google.common.base.Preconditions;
-
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.support.annotation.Nullable;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.LayoutManager;
-import android.support.v7.widget.RecyclerView.OnScrollListener;
-import android.util.SparseBooleanArray;
-import android.view.View;
-
-/**
- * Provides a band selection item model for views within a RecyclerView. This class queries the
- * RecyclerView to determine where its items are placed; then, once band selection is underway, it
- * alerts listeners of which items are covered by the selections.
- */
-public final class BandSelectMatrix extends RecyclerView.OnScrollListener {
-
-    private final RecyclerViewHelper mHelper;
-    private final List<OnSelectionChangedListener> mOnSelectionChangedListeners = new ArrayList<>();
-
-    // Map from the x-value of the left side of an item to an ordered list of metadata of all items
-    // whose x-values are the same. The list is ordered by the y-values of the items in the column.
-    // For example, if the first column of the view starts at an x-value of 5, mColumns.get(5) would
-    // return a list of all items in that column, with the top-most item first in the list and the
-    // bottom-most item last in the list.
-    private final Map<Integer, List<ItemData>> mColumns = new HashMap<>();
-
-    // List of limits along the x-axis. For example, if the view has two columns, this list will
-    // have two elements, each of which lists the lower- and upper-limits of the x-values of the
-    // view items. This list is sorted from furthest left to furthest right.
-    private final List<Limits> mXLimitsList = new ArrayList<>();
-
-    // Like mXLimitsList, but for y-coordinates. Note that this list only contains items which have
-    // been in the viewport. Thus, limits which exist in an area of the view to which the view has
-    // not scrolled are not present in the list.
-    private final List<Limits> mYLimitsList = new ArrayList<>();
-
-    // The adapter positions which have been recorded so far.
-    private final SparseBooleanArray mRecordedPositions = new SparseBooleanArray();
-
-    // Array passed to registered OnSelectionChangedListeners. One array is created and reused
-    // throughout the lifetime of the object.
-    private final SparseBooleanArray mSelectionForListeners = new SparseBooleanArray();
-
-    // The current pointer (in absolute positioning from the top of the view).
-    private Point mPointer = null;
-
-    // The bounds of the band selection.
-    private RelativePoint mRelativeOrigin;
-    private RelativePoint mRelativePointer;
-
-    BandSelectMatrix(RecyclerViewHelper helper) {
-        mHelper = helper;
-        mHelper.addOnScrollListener(this);
-    }
-
-    BandSelectMatrix(RecyclerView rv) {
-        this(new RuntimeRecyclerViewHelper(rv));
-    }
-
-    /**
-     * Stops listening to the view's scrolls. Call this function before discarding a
-     * BandSelecMatrix object to prevent memory leaks.
-     */
-    void stopListening() {
-        mHelper.removeOnScrollListener(this);
-    }
-
-    /**
-     * Start a band select operation at the given point.
-     * @param relativeOrigin The origin of the band select operation, relative to the viewport.
-     *     For example, if the view is scrolled to the bottom, the top-left of the viewport would
-     *     have a relative origin of (0, 0), even though its absolute point has a higher y-value.
-     */
-    void startSelection(Point relativeOrigin) {
-        Point absoluteOrigin = mHelper.createAbsolutePoint(relativeOrigin);
-        mPointer = new Point(absoluteOrigin.x, absoluteOrigin.y);
-
-        processVisibleChildren();
-        mRelativeOrigin = new RelativePoint(absoluteOrigin);
-        mRelativePointer = new RelativePoint(mPointer);
-        computeCurrentSelection();
-        notifyListeners();
-    }
-
-    /**
-     * Resizes the selection by adjusting the pointer (i.e., the corner of the selection opposite
-     * the origin.
-     * @param relativePointer The pointer (opposite of the origin) of the band select operation,
-     *     relative to the viewport. For example, if the view is scrolled to the bottom, the
-     *     top-left of the viewport would have a relative origin of (0, 0), even though its absolute
-     *     point has a higher y-value.
-     */
-    void resizeSelection(Point relativePointer) {
-        mPointer = mHelper.createAbsolutePoint(relativePointer);
-        handlePointerMoved();
-    }
-
-    @Override
-    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-        if (mPointer == null) {
-            return;
-        }
-
-        mPointer.x += dx;
-        mPointer.y += dy;
-        processVisibleChildren();
-        handlePointerMoved();
-    }
-
-    /**
-     * Queries the view for all children and records their location metadata.
-     */
-    private void processVisibleChildren() {
-        for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
-            int adapterPosition = mHelper.getAdapterPositionAt(i);
-            if (!mRecordedPositions.get(adapterPosition)) {
-                mRecordedPositions.put(adapterPosition, true);
-                captureItemLayoutData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
-            }
-        }
-    }
-
-    /**
-     * Updates the limits lists and column map with the given item metadata.
-     * @param absoluteChildRect The absolute rectangle for the child view being processed.
-     * @param adapterPosition The position of the child view being processed.
-     */
-    private void captureItemLayoutData(Rect absoluteChildRect, int adapterPosition) {
-        if (mXLimitsList.size() != mHelper.getNumColumns()) {
-            // If not all x-limits have been recorded, record this one.
-            recordLimits(
-                    mXLimitsList, new Limits(absoluteChildRect.left, absoluteChildRect.right));
-        }
-
-        if (mYLimitsList.size() != mHelper.getNumRows()) {
-            // If not all y-limits have been recorded, record this one.
-            recordLimits(
-                    mYLimitsList, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
-        }
-
-        List<ItemData> columnList = mColumns.get(absoluteChildRect.left);
-        if (columnList == null) {
-            columnList = new ArrayList<ItemData>();
-            mColumns.put(absoluteChildRect.left, columnList);
-        }
-        recordItemData(
-                columnList, new ItemData(adapterPosition, absoluteChildRect.top));
-    }
-
-    /**
-     * Ensures limits exists within the sorted list limitsList, and adds it to the list if it does
-     * not exist.
-     */
-    private static void recordLimits(List<Limits> limitsList, Limits limits) {
-        int index = Collections.binarySearch(limitsList, limits);
-        if (index < 0) {
-            limitsList.add(~index, limits);
-        }
-    }
-
-    /**
-     * Ensures itemData exists within the sorted list itemDataList, and adds it to the list if it
-     * does not exist.
-     */
-    private static void recordItemData(List<ItemData> itemDataList, ItemData itemData) {
-        int index = Collections.binarySearch(itemDataList, itemData);
-        if (index < 0) {
-            itemDataList.add(~index, itemData);
-        }
-    }
-
-    /**
-     * Handles a moved pointer; this function determines whether the pointer movement resulted in a
-     * selection change and, if it has, notifies listeners of this change.
-     */
-    private void handlePointerMoved() {
-        RelativePoint old = mRelativePointer;
-        mRelativePointer = new RelativePoint(mPointer);
-        if (old != null && mRelativePointer.equals(old)) {
-            return;
-        }
-
-        computeCurrentSelection();
-        notifyListeners();
-    }
-
-    /**
-     * Computes the currently-selected items.
-     */
-    private void computeCurrentSelection() {
-        Rect selectionRect = mRelativePointer.computeBounds(mRelativeOrigin);
-        computePositionsCoveredByRect(selectionRect);
-    }
-
-    /**
-     * Notifies all listeners of a selection change. Note that this function simply passes
-     * mSelectionForListeners, so computeCurrentSelection() should be called before this function.
-     */
-    private void notifyListeners() {
-        for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
-            listener.onSelectionChanged(mSelectionForListeners);
-        }
-    }
-
-    /**
-     * @param rect Rectangle including all covered items.
-     */
-    private void computePositionsCoveredByRect(@Nullable Rect rect) {
-        mSelectionForListeners.clear();
-        if (rect == null) {
-            // If there is no bounding rectangle, there are no items selected, so just return early.
-            return;
-        }
-
-        int columnIndex = Collections.binarySearch(mXLimitsList, new Limits(rect.left, rect.left));
-        Preconditions.checkState(columnIndex >= 0);
-
-        for (; columnIndex < mXLimitsList.size() &&
-                mXLimitsList.get(columnIndex).lowerLimit <= rect.right; columnIndex++) {
-            List<ItemData> positions =
-                    mColumns.get(mXLimitsList.get(columnIndex).lowerLimit);
-            int rowIndex = Collections.binarySearch(positions, new ItemData(0, rect.top));
-            if (rowIndex < 0) {
-                // If band select occurs after the last item in a row with fewer items than columns,
-                // go to the next column. This situation occurs in the last row of the grid when the
-                // total number of items is not a multiple of the number of columns (e.g., when 10
-                // items exist in a grid with 4 columns).
-                continue;
-            }
-
-            for (; rowIndex < positions.size() &&
-                    positions.get(rowIndex).offset <= rect.bottom; rowIndex++) {
-                mSelectionForListeners.append(positions.get(rowIndex).position, true);
-            }
-        }
-    }
-
-    /**
-     * Provides functionality for interfacing with the view. In practice, RecyclerViewMatrixHelper
-     * should be used; this interface exists solely for the purpose of decoupling the view from
-     * this class so that the view can be mocked out for tests.
-     */
-    interface RecyclerViewHelper {
-        public void addOnScrollListener(RecyclerView.OnScrollListener listener);
-        public void removeOnScrollListener(RecyclerView.OnScrollListener listener);
-        public Point createAbsolutePoint(Point relativePoint);
-        public int getVisibleChildCount();
-        public int getTotalChildCount();
-        public int getNumColumns();
-        public int getNumRows();
-        public int getAdapterPositionAt(int index);
-        public Rect getAbsoluteRectForChildViewAt(int index);
-    }
-
-    /**
-     * Concrete MatrixHelper implementation for use within the Files app.
-     */
-    static class RuntimeRecyclerViewHelper implements RecyclerViewHelper {
-        private final RecyclerView mRecyclerView;
-
-        RuntimeRecyclerViewHelper(RecyclerView rv) {
-            mRecyclerView = rv;
-        }
-
-        @Override
-        public int getAdapterPositionAt(int index) {
-            View child = mRecyclerView.getChildAt(index);
-            return mRecyclerView.getChildViewHolder(child).getAdapterPosition();
-        }
-
-        @Override
-        public void addOnScrollListener(OnScrollListener listener) {
-            mRecyclerView.addOnScrollListener(listener);
-        }
-
-        @Override
-        public void removeOnScrollListener(OnScrollListener listener) {
-            mRecyclerView.removeOnScrollListener(listener);
-        }
-
-        @Override
-        public Point createAbsolutePoint(Point relativePoint) {
-            return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
-                    relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
-        }
-
-        @Override
-        public Rect getAbsoluteRectForChildViewAt(int index) {
-            final View child = mRecyclerView.getChildAt(index);
-            final Rect childRect = new Rect();
-            child.getHitRect(childRect);
-            childRect.left += mRecyclerView.computeHorizontalScrollOffset();
-            childRect.right += mRecyclerView.computeHorizontalScrollOffset();
-            childRect.top += mRecyclerView.computeVerticalScrollOffset();
-            childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
-            return childRect;
-        }
-
-        @Override
-        public int getVisibleChildCount() {
-            return mRecyclerView.getChildCount();
-        }
-
-        @Override
-        public int getTotalChildCount() {
-            return mRecyclerView.getAdapter().getItemCount();
-        }
-
-        @Override
-        public int getNumColumns() {
-            LayoutManager layoutManager = mRecyclerView.getLayoutManager();
-            if (layoutManager instanceof GridLayoutManager) {
-                return ((GridLayoutManager) layoutManager).getSpanCount();
-            }
-
-            // Otherwise, it is a list with 1 column.
-            return 1;
-        }
-
-        @Override
-        public int getNumRows() {
-            int numFullColumns = getTotalChildCount() / getNumColumns();
-            boolean hasPartiallyFullColumn = getTotalChildCount() % getNumColumns() != 0;
-            return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
-        }
-    }
-
-    /**
-     * Listener for changes in which items have been band selected.
-     */
-    interface OnSelectionChangedListener {
-        public void onSelectionChanged(SparseBooleanArray updatedSelection);
-    }
-
-    void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
-        mOnSelectionChangedListeners.add(listener);
-    }
-
-    void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
-        mOnSelectionChangedListeners.remove(listener);
-    }
-
-    /**
-     * Metadata for an item in the view, consisting of the adapter position and the offset from the
-     * top of the view (in pixels). Stored in the mColumns map to model the item grid.
-     */
-    private static class ItemData implements Comparable<ItemData> {
-        int position;
-        int offset;
-
-        ItemData(int position, int offset) {
-            this.position = position;
-            this.offset = offset;
-        }
-
-        @Override
-        public int compareTo(ItemData other) {
-            // The list of columns is sorted via the offset from the top, so PositionMetadata
-            // objects with lower y-values are befor those with higher y-values.
-            return offset - other.offset;
-        }
-    }
-
-    /**
-     * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
-     * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
-     * of item columns and the top- and bottom sides of item rows so that it can be determined
-     * whether the pointer is located within the bounds of an item.
-     */
-    private static class Limits implements Comparable<Limits> {
-        int lowerLimit;
-        int upperLimit;
-
-        Limits(int lowerLimit, int upperLimit) {
-            this.lowerLimit = lowerLimit;
-            this.upperLimit = upperLimit;
-        }
-
-        @Override
-        public int compareTo(Limits other) {
-            return lowerLimit - other.lowerLimit;
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (!(other instanceof Limits)) {
-                return false;
-            }
-
-            return ((Limits) other).lowerLimit == lowerLimit &&
-                    ((Limits) other).upperLimit == upperLimit;
-        }
-    }
-
-    /**
-     * The location of a coordinate relative to items. This class represents a general area of the
-     * view as it relates to band selection rather than an explicit point. For example, two
-     * different points within an item are considered to have the same "location" because band
-     * selection originating within the item would select the same items no matter which point
-     * was used. Same goes for points between items as well as those at the very beginning or end
-     * of the view.
-     *
-     * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
-     * advantage of tying the value to the Limits of items along that axis. This allows easy
-     * selection of items within those Limits as opposed to a search through every item to see if a
-     * given coordinate value falls within those Limits.
-     */
-    private static class RelativeCoordinate
-            implements Comparable<RelativeCoordinate> {
-        /**
-         * Location describing points after the last known item.
-         */
-        static final int AFTER_LAST_ITEM = 0;
-
-        /**
-         * Location describing points before the first known item.
-         */
-        static final int BEFORE_FIRST_ITEM = 1;
-
-        /**
-         * Location describing points between two items.
-         */
-        static final int BETWEEN_TWO_ITEMS = 2;
-
-        /**
-         * Location describing points within the limits of one item.
-         */
-        static final int WITHIN_LIMITS = 3;
-
-        /**
-         * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
-         * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
-         */
-        final int type;
-
-        /**
-         * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
-         * BETWEEN_TWO_ITEMS.
-         */
-        Limits limitsBeforeCoordinate;
-
-        /**
-         * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
-         */
-        Limits limitsAfterCoordinate;
-
-        // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
-        Limits mFirstKnownItem;
-        // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
-        Limits mLastKnownItem;
-
-        /**
-         * @param limitsList The sorted limits list for the coordinate type. If this
-         *     CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
-         *     mYLimitsList should be pased.
-         * @param value The coordinate value.
-         */
-        RelativeCoordinate(List<Limits> limitsList, int value) {
-            Limits dummyLimits = new Limits(value, value);
-            int index = Collections.binarySearch(limitsList, dummyLimits);
-
-            if (index >= 0) {
-                this.type = WITHIN_LIMITS;
-                this.limitsBeforeCoordinate = limitsList.get(index);
-            } else if (~index == 0) {
-                this.type = BEFORE_FIRST_ITEM;
-                this.mFirstKnownItem = limitsList.get(0);
-            } else if (~index == limitsList.size()) {
-                Limits lastLimits = limitsList.get(limitsList.size() - 1);
-                if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
-                    this.type = WITHIN_LIMITS;
-                    this.limitsBeforeCoordinate = lastLimits;
-                } else {
-                    this.type = AFTER_LAST_ITEM;
-                    this.mLastKnownItem = lastLimits;
-                }
-            } else {
-                Limits limitsBeforeIndex = limitsList.get(~index - 1);
-                if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
-                    this.type = WITHIN_LIMITS;
-                    this.limitsBeforeCoordinate = limitsList.get(~index - 1);
-                } else {
-                    this.type = BETWEEN_TWO_ITEMS;
-                    this.limitsBeforeCoordinate = limitsList.get(~index - 1);
-                    this.limitsAfterCoordinate = limitsList.get(~index);
-                }
-            }
-        }
-
-        int toComparisonValue() {
-            if (type == BEFORE_FIRST_ITEM) {
-                return mFirstKnownItem.lowerLimit - 1;
-            } else if (type == AFTER_LAST_ITEM) {
-                return mLastKnownItem.upperLimit + 1;
-            } else if (type == BETWEEN_TWO_ITEMS) {
-                return limitsBeforeCoordinate.upperLimit + 1;
-            } else {
-                return limitsBeforeCoordinate.lowerLimit;
-            }
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (!(other instanceof RelativeCoordinate)) {
-                return false;
-            }
-
-            RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
-            return toComparisonValue() == otherCoordinate.toComparisonValue();
-        }
-
-        @Override
-        public int compareTo(RelativeCoordinate other) {
-            return toComparisonValue() - other.toComparisonValue();
-        }
-    }
-
-    /**
-     * The location of a point relative to the Limits of nearby items; consists of both an x- and
-     * y-RelativeCoordinateLocation.
-     */
-    private class RelativePoint {
-        final RelativeCoordinate xLocation;
-        final RelativeCoordinate yLocation;
-
-        RelativePoint(Point point) {
-            this.xLocation = new RelativeCoordinate(mXLimitsList, point.x);
-            this.yLocation = new RelativeCoordinate(mYLimitsList, point.y);
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (!(other instanceof RelativePoint)) {
-                return false;
-            }
-
-            RelativePoint otherPoint = (RelativePoint) other;
-            return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
-        }
-
-        /**
-         * Generates a rectangle which contains the items selected by the two points.
-         * @param other The other PointLocation. A rectangle will be formed between "this" and
-         *     "other".
-         * @return The rectangle, or null if no items were selected.
-         */
-        Rect computeBounds(RelativePoint other) {
-            if (!areItemsCoveredBySelection(mRelativePointer, mRelativeOrigin)) {
-                return null;
-            }
-
-            RelativeCoordinate minXLocation =
-                    xLocation.compareTo(other.xLocation) < 0 ? xLocation : other.xLocation;
-            RelativeCoordinate maxXLocation =
-                    minXLocation == xLocation ? other.xLocation : xLocation;
-            RelativeCoordinate minYLocation =
-                    yLocation.compareTo(other.yLocation) < 0 ? yLocation : other.yLocation;
-            RelativeCoordinate maxYLocation =
-                    minYLocation == yLocation ? other.yLocation : yLocation;
-
-            Rect rect = new Rect();
-            rect.left = getCoordinateValue(minXLocation, mXLimitsList, true);
-            rect.right = getCoordinateValue(maxXLocation, mXLimitsList, false);
-            rect.top = getCoordinateValue(minYLocation, mYLimitsList, true);
-            rect.bottom = getCoordinateValue(maxYLocation, mYLimitsList, false);
-            return rect;
-        }
-
-        int getCoordinateValue(RelativeCoordinate coordinate,
-                List<Limits> limitsList, boolean isStartOfRange) {
-            switch (coordinate.type) {
-                case RelativeCoordinate.BEFORE_FIRST_ITEM:
-                    return limitsList.get(0).lowerLimit;
-                case RelativeCoordinate.AFTER_LAST_ITEM:
-                    return limitsList.get(limitsList.size() - 1).upperLimit;
-                case RelativeCoordinate.BETWEEN_TWO_ITEMS:
-                    if (isStartOfRange) {
-                        return coordinate.limitsAfterCoordinate.lowerLimit;
-                    } else {
-                        return coordinate.limitsBeforeCoordinate.upperLimit;
-                    }
-                case RelativeCoordinate.WITHIN_LIMITS:
-                    return coordinate.limitsBeforeCoordinate.lowerLimit;
-            }
-
-            throw new RuntimeException("Invalid coordinate value.");
-        }
-    }
-
-    private static boolean areItemsCoveredBySelection(
-            RelativePoint first, RelativePoint second) {
-        return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
-                doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
-    }
-
-    private static boolean doesCoordinateLocationCoverItems(
-            RelativeCoordinate pointerCoordinate,
-            RelativeCoordinate originCoordinate) {
-        if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
-                originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
-            return false;
-        }
-
-        if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
-                originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
-            return false;
-        }
-
-        if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
-                originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
-                pointerCoordinate.limitsBeforeCoordinate.equals(originCoordinate) &&
-                pointerCoordinate.limitsAfterCoordinate.equals(originCoordinate)) {
-            return false;
-        }
-
-        return true;
-    }
-}
index 240cdda..c28806b 100644 (file)
@@ -1773,8 +1773,6 @@ public class DirectoryFragment extends Fragment {
         }
 
         @Override
-        public void afterActivityCreated(DirectoryFragment fragment) {
-            new BandSelectManager(fragment.mRecView, fragment.mSelectionManager);
-        }
+        public void afterActivityCreated(DirectoryFragment fragment) {}
     }
 }
index f87fe4c..e53168d 100644 (file)
 
 package com.android.documentsui;
 
+import static com.android.documentsui.Events.isMouseEvent;
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
 
+import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.Adapter;
 import android.support.v7.widget.RecyclerView.AdapterDataObserver;
+import android.support.v7.widget.RecyclerView.LayoutManager;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
 import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.GestureDetector;
@@ -30,11 +34,16 @@ import android.view.GestureDetector.OnDoubleTapListener;
 import android.view.GestureDetector.OnGestureListener;
 import android.view.MotionEvent;
 import android.view.View;
-
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.support.annotation.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
@@ -61,12 +70,13 @@ public final class MultiSelectManager {
     private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
 
     private Adapter<?> mAdapter;
-    private RecyclerViewHelper mHelper;
+    private MultiSelectHelper mHelper;
     private boolean mSingleSelect;
+    private BandSelectManager mBandSelectManager;
 
     /**
      * @param recyclerView
-     * @param gestureDelegate Option delage gesture listener.
+     * @param gestureDelegate Option delegate gesture listener.
      * @param mode Selection mode
      * @template A gestureDelegate that implements both {@link OnGestureListener}
      *     and {@link OnDoubleTapListener}
@@ -76,17 +86,11 @@ public final class MultiSelectManager {
 
         this(
                 recyclerView.getAdapter(),
-                new RecyclerViewHelper() {
-                    @Override
-                    public int findEventPosition(MotionEvent e) {
-                        View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
-                        return view != null
-                                ? recyclerView.getChildAdapterPosition(view)
-                                : RecyclerView.NO_POSITION;
-                    }
-                },
+                new RuntimeRecyclerViewHelper(recyclerView),
                 mode);
 
+        mBandSelectManager = new BandSelectManager((RuntimeRecyclerViewHelper) mHelper);
+
         GestureDetector.SimpleOnGestureListener listener =
                 new GestureDetector.SimpleOnGestureListener() {
                     @Override
@@ -101,11 +105,8 @@ public final class MultiSelectManager {
 
         CompositeOnGestureListener<? extends Object> compositeListener =
                 new CompositeOnGestureListener<>(listener, gestureDelegate);
-        final GestureDetector detector = new GestureDetector(
-                recyclerView.getContext(),
-                gestureDelegate == null
-                        ? listener
-                        : compositeListener);
+        final GestureDetector detector =
+                new GestureDetector(recyclerView.getContext(), compositeListener);
 
         detector.setOnDoubleTapListener(compositeListener);
 
@@ -113,9 +114,15 @@ public final class MultiSelectManager {
                 new RecyclerView.OnItemTouchListener() {
                     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                         detector.onTouchEvent(e);
-                        return false;
+
+                        // Only intercept the event if it was a mouse-based band selection.
+                        return isMouseEvent(e) && (mBandSelectManager.mIsActive ||
+                                e.getActionMasked() != MotionEvent.ACTION_UP);
+                    }
+                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+                        checkState(isMouseEvent(e));
+                        mBandSelectManager.processMotionEvent(e);
                     }
-                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
                     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
                 });
     }
@@ -125,7 +132,7 @@ public final class MultiSelectManager {
      * @hide
      */
     @VisibleForTesting
-    MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper, int mode) {
+    MultiSelectManager(Adapter<?> adapter, MultiSelectHelper helper, int mode) {
         checkNotNull(adapter, "'adapter' cannot be null.");
         checkNotNull(helper, "'helper' cannot be null.");
 
@@ -207,6 +214,7 @@ public final class MultiSelectManager {
      * @param selected
      * @return True if the selection state of the item changed.
      */
+    @VisibleForTesting
     public boolean setItemSelected(int position, boolean selected) {
         if (mSingleSelect && !mSelection.isEmpty()) {
             clearSelectionQuietly();
@@ -271,7 +279,7 @@ public final class MultiSelectManager {
             if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
         }
 
-        onLongPress(position);
+        onLongPress(position, e.getMetaState());
     }
 
     /**
@@ -279,17 +287,16 @@ public final class MultiSelectManager {
      * can be mocked.
      *
      * @param position
+     * @param metaState as returned from {@link MotionEvent#getMetaState()}.
      * @hide
      */
     @VisibleForTesting
-    void onLongPress(int position) {
+    void onLongPress(int position, int metaState) {
         if (position == RecyclerView.NO_POSITION) {
             if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
         }
 
-        if (toggleSelection(position)) {
-            notifySelectionChanged();
-        }
+        handlePositionChanged(position, metaState);
     }
 
     /**
@@ -327,17 +334,26 @@ public final class MultiSelectManager {
             return false;
         }
 
+        handlePositionChanged(position, metaState);
+        return false;
+    }
+
+    /**
+     * Handles a change caused by a click on the item with the given position. If the Shift key is
+     * held down, this performs a range select; otherwise, it simply toggles the item's selection
+     * state.
+     */
+    private void handlePositionChanged(int position, int metaState) {
         if (Events.hasShiftBit(metaState) && mRanger != null) {
             mRanger.snapSelection(position);
-        } else {
-            toggleSelection(position);
-        }
 
-        // We're being lazy here notifying even when something might not have changed.
-        // To make this more correct, we'd need to update the Ranger class to return
-        // information about what has changed.
-        notifySelectionChanged();
-        return false;
+            // We're being lazy here notifying even when something might not have changed.
+            // To make this more correct, we'd need to update the Ranger class to return
+            // information about what has changed.
+            notifySelectionChanged();
+        } else if (toggleSelection(position)) {
+            notifySelectionChanged();
+        }
     }
 
     /**
@@ -588,10 +604,23 @@ public final class MultiSelectManager {
      */
     public static final class Selection {
 
-        private SparseBooleanArray mSelection;
+        // This class tracks selected positions by managing two arrays: the saved selection, and
+        // the total selection. Saved selections are those which have been completed by tapping an
+        // item or by completing a band select operation. Provisional selections are selections
+        // which have been temporarily created by an in-progress band select operation (once the
+        // user releases the mouse button during a band select operation, the selected items
+        // become saved). The total selection is the combination of both the saved selection and
+        // the provisional selection. Tracking both separately is necessary to ensure that saved
+        // selections do not become deselected when they are removed from the provisional selection;
+        // for example, if item A is tapped (and selected), then an in-progress band select covers A
+        // then uncovers A, A should still be selected as it has been saved. To ensure this
+        // behavior, the saved selection must be tracked separately.
+        private SparseBooleanArray mSavedSelection;
+        private SparseBooleanArray mTotalSelection;
 
         public Selection() {
-            mSelection = new SparseBooleanArray();
+            mSavedSelection = new SparseBooleanArray();
+            mTotalSelection = new SparseBooleanArray();
         }
 
         /**
@@ -599,7 +628,7 @@ public final class MultiSelectManager {
          * @return true if the position is currently selected.
          */
         public boolean contains(int position) {
-            return mSelection.get(position);
+            return mTotalSelection.get(position);
         }
 
         /**
@@ -613,21 +642,85 @@ public final class MultiSelectManager {
          * @return the position value stored at specified index.
          */
         public int get(int index) {
-            return mSelection.keyAt(index);
+            return mTotalSelection.keyAt(index);
         }
 
         /**
          * @return size of the selection.
          */
         public int size() {
-            return mSelection.size();
+            return mTotalSelection.size();
         }
 
         /**
          * @return true if the selection is empty.
          */
         public boolean isEmpty() {
-            return mSelection.size() == 0;
+            return mTotalSelection.size() == 0;
+        }
+
+        /**
+         * Sets the provisional selection, which is a temporary selection that can be saved,
+         * canceled, or adjusted at a later time. When a new provision selection is applied, the old
+         * one (if it exists) is abandoned.
+         * @return Array with entry for each position added or removed. Entries which were added
+         *     contain a value of true, and entries which were removed contain a value of false.
+         */
+        @VisibleForTesting
+        protected SparseBooleanArray setProvisionalSelection(
+                SparseBooleanArray provisionalSelection) {
+            SparseBooleanArray delta = new SparseBooleanArray();
+
+            for (int i = 0; i < mTotalSelection.size(); i++) {
+                int position = mTotalSelection.keyAt(i);
+                if (!provisionalSelection.get(position) && !mSavedSelection.get(position)) {
+                    // Remove each item that used to be in the selection but is unsaved and not in
+                    // the new provisional selection.
+                    delta.put(position, false);
+                }
+            }
+
+            for (int i = 0; i < provisionalSelection.size(); i++) {
+                int position = provisionalSelection.keyAt(i);
+                if (!mTotalSelection.get(position)) {
+                    // Add each item that was not previously in the selection but is in the
+                    // new provisional selection.
+                    delta.put(position, true);
+                }
+            }
+
+            // Now, iterate through the changes and actually add/remove them to/from
+            // mCurrentSelection. This could not be done in the previous loops because changing the
+            // size of the selection mid-iteration changes iteration order erroneously.
+            for (int i = 0; i < delta.size(); i++) {
+                int position = delta.keyAt(i);
+                if (delta.get(position)) {
+                    mTotalSelection.put(position, true);
+                } else {
+                    mTotalSelection.delete(position);
+                }
+            }
+
+            return delta;
+        }
+
+        /**
+         * Saves the existing provisional selection. Once the provisional selection is saved,
+         * subsequent provisional selections which are different from this existing one cannot
+         * cause items in this existing provisional selection to become deselected.
+         */
+        @VisibleForTesting
+        protected void applyProvisionalSelection() {
+            mSavedSelection = mTotalSelection.clone();
+        }
+
+        /**
+         * Abandons the existing provisional selection so that all items provisionally selected are
+         * now deselected.
+         */
+        @VisibleForTesting
+        protected void cancelProvisionalSelection() {
+            mTotalSelection = mSavedSelection.clone();
         }
 
         private boolean flip(int position) {
@@ -643,8 +736,9 @@ public final class MultiSelectManager {
         /** @hide */
         @VisibleForTesting
         boolean add(int position) {
-            if (!mSelection.get(position)) {
-                mSelection.put(position, true);
+            if (!mTotalSelection.get(position)) {
+                mTotalSelection.put(position, true);
+                mSavedSelection.put(position, true);
                 return true;
             }
             return false;
@@ -653,8 +747,9 @@ public final class MultiSelectManager {
         /** @hide */
         @VisibleForTesting
         boolean remove(int position) {
-            if (mSelection.get(position)) {
-                mSelection.delete(position);
+            if (mTotalSelection.get(position)) {
+                mTotalSelection.delete(position);
+                mSavedSelection.delete(position);
                 return true;
             }
             return false;
@@ -663,7 +758,8 @@ public final class MultiSelectManager {
         /**
          * Adjusts the selection range to reflect the existence of newly inserted values at
          * the specified positions. This has the effect of adjusting all existing selected
-         * positions within the specified range accordingly.
+         * positions within the specified range accordingly. Note that this function discards any
+         * provisional selections which may have been applied.
          *
          * @param startPosition
          * @param count
@@ -673,11 +769,13 @@ public final class MultiSelectManager {
         void expand(int startPosition, int count) {
             checkState(startPosition >= 0);
             checkState(count > 0);
+            cancelProvisionalSelection();
 
-            for (int i = 0; i < mSelection.size(); i++) {
-                int itemPosition = mSelection.keyAt(i);
+            for (int i = 0; i < mTotalSelection.size(); i++) {
+                int itemPosition = mTotalSelection.keyAt(i);
                 if (itemPosition >= startPosition) {
-                    mSelection.setKeyAt(i, itemPosition + count);
+                    mTotalSelection.setKeyAt(i, itemPosition + count);
+                    mSavedSelection.setKeyAt(i, itemPosition + count);
                 }
             }
         }
@@ -685,7 +783,8 @@ public final class MultiSelectManager {
         /**
          * Adjusts the selection range to reflect the removal specified positions. This has
          * the effect of adjusting all existing selected positions within the specified range
-         * accordingly.
+         * accordingly. Note that this function discards any provisional selections which may have
+         * been applied.
          *
          * @param startPosition
          * @param count The length of the range to collapse. Must be greater than 0.
@@ -699,27 +798,30 @@ public final class MultiSelectManager {
             int endPosition = startPosition + count - 1;
 
             SparseBooleanArray newSelection = new SparseBooleanArray();
-            for (int i = 0; i < mSelection.size(); i++) {
-                int itemPosition = mSelection.keyAt(i);
+            for (int i = 0; i < mSavedSelection.size(); i++) {
+                int itemPosition = mSavedSelection.keyAt(i);
                 if (itemPosition < startPosition) {
                     newSelection.append(itemPosition, true);
                 } else if (itemPosition > endPosition) {
                     newSelection.append(itemPosition - count, true);
                 }
             }
-            mSelection = newSelection;
+            mSavedSelection = newSelection;
+            cancelProvisionalSelection();
         }
 
         /** @hide */
         @VisibleForTesting
         void clear() {
-            mSelection.clear();
+            mSavedSelection.clear();
+            mTotalSelection.clear();
         }
 
         /** @hide */
         @VisibleForTesting
         void copyFrom(Selection source) {
-            mSelection = source.mSelection.clone();
+            mSavedSelection = source.mSavedSelection.clone();
+            mTotalSelection = source.mTotalSelection.clone();
         }
 
         @Override
@@ -728,16 +830,16 @@ public final class MultiSelectManager {
                 return "size=0, items=[]";
             }
 
-            StringBuilder buffer = new StringBuilder(mSelection.size() * 28);
+            StringBuilder buffer = new StringBuilder(mTotalSelection.size() * 28);
             buffer.append("{size=")
-                    .append(mSelection.size())
+                    .append(mTotalSelection.size())
                     .append(", ")
                     .append("items=[");
-            for (int i=0; i < mSelection.size(); i++) {
+            for (int i=0; i < mTotalSelection.size(); i++) {
                 if (i > 0) {
                     buffer.append(", ");
                 }
-                buffer.append(mSelection.keyAt(i));
+                buffer.append(mTotalSelection.keyAt(i));
             }
             buffer.append("]}");
             return buffer.toString();
@@ -745,7 +847,7 @@ public final class MultiSelectManager {
 
         @Override
         public int hashCode() {
-            return mSelection.hashCode();
+            return mSavedSelection.hashCode() ^ mTotalSelection.hashCode();
         }
 
         @Override
@@ -758,12 +860,175 @@ public final class MultiSelectManager {
               return false;
           }
 
-          return mSelection.equals(((Selection) that).mSelection);
+          return mSavedSelection.equals(((Selection) that).mSavedSelection) &&
+                  mTotalSelection.equals(((Selection) that).mTotalSelection);
         }
     }
 
-    interface RecyclerViewHelper {
+    /**
+     * Provides functionality for MultiSelectManager. In practice, use RuntimeRecyclerViewHelper;
+     * this interface exists only for mocking in tests.
+     */
+    interface MultiSelectHelper {
+        int findEventPosition(MotionEvent e);
+    }
+
+    /**
+     * Provides functionality for BandSelectManager. In practice, use RuntimeRecyclerViewHelper;
+     * this interface exists only for mocking in tests.
+     */
+    interface BandManagerHelper {
+        void drawBand(Rect rect);
         int findEventPosition(MotionEvent e);
+        int getHeight();
+        void hideBand();
+        void invalidateView();
+        void postRunnable(Runnable r);
+        void removeCallback(Runnable r);
+        void scrollBy(int dy);
+    }
+
+    /**
+     * Provides functionality for BandSelectModel. In practice, use RuntimeRecyclerViewHelper;
+     * this interface exists only for mocking in tests.
+     */
+    interface BandModelHelper {
+        void addOnScrollListener(RecyclerView.OnScrollListener listener);
+        Point createAbsolutePoint(Point relativePoint);
+        Rect getAbsoluteRectForChildViewAt(int index);
+        int getAdapterPositionAt(int index);
+        int getNumColumns();
+        int getNumRows();
+        int getTotalChildCount();
+        int getVisibleChildCount();
+        void removeOnScrollListener(RecyclerView.OnScrollListener listener);
+    }
+
+    /**
+     * Concrete RecyclerViewHelper implementation for use within the Files app.
+     */
+    private static final class RuntimeRecyclerViewHelper implements MultiSelectHelper,
+            BandManagerHelper, BandModelHelper {
+
+        private final RecyclerView mRecyclerView;
+        private final Drawable mBandSelectRect;
+
+        private boolean mIsOverlayShown = false;
+
+        RuntimeRecyclerViewHelper(RecyclerView rv) {
+            mRecyclerView = rv;
+            mBandSelectRect = mRecyclerView.getContext().getTheme().getDrawable(
+                    R.drawable.band_select_overlay);
+        }
+
+        @Override
+        public int getAdapterPositionAt(int index) {
+            View child = mRecyclerView.getChildAt(index);
+            return mRecyclerView.getChildViewHolder(child).getAdapterPosition();
+        }
+
+        @Override
+        public void addOnScrollListener(OnScrollListener listener) {
+            mRecyclerView.addOnScrollListener(listener);
+        }
+
+        @Override
+        public void removeOnScrollListener(OnScrollListener listener) {
+            mRecyclerView.removeOnScrollListener(listener);
+        }
+
+        @Override
+        public Point createAbsolutePoint(Point relativePoint) {
+            return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
+                    relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
+        }
+
+        @Override
+        public Rect getAbsoluteRectForChildViewAt(int index) {
+            final View child = mRecyclerView.getChildAt(index);
+            final Rect childRect = new Rect();
+            child.getHitRect(childRect);
+            childRect.left += mRecyclerView.computeHorizontalScrollOffset();
+            childRect.right += mRecyclerView.computeHorizontalScrollOffset();
+            childRect.top += mRecyclerView.computeVerticalScrollOffset();
+            childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
+            return childRect;
+        }
+
+        @Override
+        public int getVisibleChildCount() {
+            return mRecyclerView.getChildCount();
+        }
+
+        @Override
+        public int getTotalChildCount() {
+            return mRecyclerView.getAdapter().getItemCount();
+        }
+
+        @Override
+        public int getNumColumns() {
+            LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+            if (layoutManager instanceof GridLayoutManager) {
+                return ((GridLayoutManager) layoutManager).getSpanCount();
+            }
+
+            // Otherwise, it is a list with 1 column.
+            return 1;
+        }
+
+        @Override
+        public int getNumRows() {
+            int numFullColumns = getTotalChildCount() / getNumColumns();
+            boolean hasPartiallyFullColumn = getTotalChildCount() % getNumColumns() != 0;
+            return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
+        }
+
+        @Override
+        public int findEventPosition(MotionEvent e) {
+            View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
+            return view != null
+                    ? mRecyclerView.getChildAdapterPosition(view)
+                    : RecyclerView.NO_POSITION;
+        }
+
+        @Override
+        public int getHeight() {
+            return mRecyclerView.getHeight();
+        }
+
+        @Override
+        public void invalidateView() {
+            mRecyclerView.invalidate();
+        }
+
+        @Override
+        public void postRunnable(Runnable r) {
+            mRecyclerView.postOnAnimation(r);
+        }
+
+        @Override
+        public void removeCallback(Runnable r) {
+            mRecyclerView.removeCallbacks(r);
+        }
+
+        @Override
+        public void scrollBy(int dy) {
+            mRecyclerView.scrollBy(0, dy);
+        }
+
+        @Override
+        public void drawBand(Rect rect) {
+            mBandSelectRect.setBounds(rect);
+
+            if (!mIsOverlayShown) {
+                mRecyclerView.getOverlay().add(mBandSelectRect);
+            }
+        }
+
+        @Override
+        public void hideBand() {
+            mRecyclerView.getOverlay().remove(mBandSelectRect);
+        }
     }
 
     public interface Callback {
@@ -893,4 +1158,863 @@ public final class MultiSelectManager {
             return false;
         }
     }
+
+    /**
+     * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
+     * and {@link MultiSelectManager}. This class is responsible for rendering the band select
+     * overlay and selecting overlaid items via MultiSelectManager.
+     */
+    public class BandSelectManager implements BandSelectModel.OnSelectionChangedListener {
+
+        private static final int NOT_SET = -1;
+
+        private final BandManagerHelper mHelper;
+
+        private boolean mIsActive;
+        private Point mOrigin;
+        private Point mPointer;
+        private Rect mBounds;
+        private BandSelectModel mModel;
+
+        // The time at which the current band selection-induced scroll began. If no scroll is in
+        // progress, the value is NOT_SET.
+        private long mScrollStartTime = NOT_SET;
+        private final Runnable mViewScroller = new ViewScroller();
+
+        public <T extends BandManagerHelper & BandModelHelper>
+                BandSelectManager(T helper) {
+            mHelper = helper;
+            mModel = new BandSelectModel(helper);
+            mModel.addOnSelectionChangedListener(this);
+        }
+
+        /**
+         * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
+         * @param e
+         */
+        private void processMotionEvent(MotionEvent e) {
+            if (!isMouseEvent(e)) {
+                return;
+            }
+
+            if (mIsActive && e.getActionMasked() == MotionEvent.ACTION_UP) {
+                endBandSelect();
+                return;
+            }
+
+            mPointer = new Point((int) e.getX(), (int) e.getY());
+            if (!mIsActive) {
+                // Only start a band select if the pointer is in margins between items, not
+                // actually within an item's bounds.
+                if (mHelper.findEventPosition(e) != RecyclerView.NO_POSITION) {
+                    return;
+                }
+                startBandSelect();
+            } else {
+                mModel.resizeSelection(mPointer);
+            }
+
+            scrollViewIfNecessary();
+            resizeBandSelectRectangle();
+        }
+
+        /**
+         * Starts band select by adding the drawable to the RecyclerView's overlay.
+         */
+        private void startBandSelect() {
+            if (DEBUG) {
+                Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");
+            }
+            mIsActive = true;
+            mOrigin = new Point(mPointer.x, mPointer.y);
+            mModel.startSelection(mOrigin);
+        }
+
+        /**
+         * Scrolls the view if necessary.
+         */
+        private void scrollViewIfNecessary() {
+            mHelper.removeCallback(mViewScroller);
+            mViewScroller.run();
+            mHelper.invalidateView();
+        }
+
+        /**
+         * Resizes the band select rectangle by using the origin and the current pointer position as
+         * two opposite corners of the selection.
+         */
+        private void resizeBandSelectRectangle() {
+            mBounds = new Rect(Math.min(mOrigin.x, mPointer.x),
+                    Math.min(mOrigin.y, mPointer.y),
+                    Math.max(mOrigin.x, mPointer.x),
+                    Math.max(mOrigin.y, mPointer.y));
+            mHelper.drawBand(mBounds);
+        }
+
+        /**
+         * Ends band select by removing the overlay.
+         */
+        private void endBandSelect() {
+            if (DEBUG) Log.d(TAG, "Ending band select.");
+            mIsActive = false;
+            mHelper.hideBand();
+            mSelection.applyProvisionalSelection();
+            mModel.endSelection();
+            int firstSelected = mModel.getPositionNearestOrigin();
+            if (firstSelected != BandSelectModel.NOT_SET) {
+                setSelectionFocusBegin(firstSelected);
+            }
+        }
+
+        @Override
+        public void onSelectionChanged(SparseBooleanArray updatedSelection) {
+            SparseBooleanArray delta = mSelection.setProvisionalSelection(updatedSelection);
+            for (int i = 0; i < delta.size(); i++) {
+                int position = delta.keyAt(i);
+                notifyItemStateChanged(position, delta.get(position));
+            }
+            notifySelectionChanged();
+        }
+
+        private class ViewScroller implements Runnable {
+            /**
+             * The number of milliseconds of scrolling at which scroll speed continues to increase.
+             * At first, the scroll starts slowly; then, the rate of scrolling increases until it
+             * reaches its maximum value at after this many milliseconds.
+             */
+            private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
+
+            @Override
+            public void run() {
+                // Compute the number of pixels the pointer's y-coordinate is past the view.
+                // Negative values mean the pointer is at or before the top of the view, and
+                // positive values mean that the pointer is at or after the bottom of the view. Note
+                // that one additional pixel is added here so that the view still scrolls when the
+                // pointer is exactly at the top or bottom.
+                int pixelsPastView = 0;
+                if (mPointer.y <= 0) {
+                    pixelsPastView = mPointer.y - 1;
+                } else if (mPointer.y >= mHelper.getHeight() - 1) {
+                    pixelsPastView = mPointer.y - mHelper.getHeight() + 1;
+                }
+
+                if (!mIsActive || pixelsPastView == 0) {
+                    // If band selection is inactive, or if it is active but not at the edge of the
+                    // view, no scrolling is necessary.
+                    mScrollStartTime = NOT_SET;
+                    return;
+                }
+
+                if (mScrollStartTime == NOT_SET) {
+                    // If the pointer was previously not at the edge of the view but now is, set the
+                    // start time for the scroll.
+                    mScrollStartTime = System.currentTimeMillis();
+                }
+
+                // Compute the number of pixels to scroll, and scroll that many pixels.
+                final int numPixels = computeScrollDistance(
+                        pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
+                mHelper.scrollBy(numPixels);
+
+                // Adjust the y-coordinate of the origin the opposite number of pixels so that the
+                // origin remains in the same place relative to the view's items.
+                mOrigin.y -= numPixels;
+                resizeBandSelectRectangle();
+
+                mHelper.removeCallback(mViewScroller);
+                mHelper.postRunnable(this);
+            }
+
+            /**
+             * Computes the number of pixels to scroll based on how far the pointer is past the end
+             * of the view and how long it has been there. Roughly based on ItemTouchHelper's
+             * algorithm for computing the number of pixels to scroll when an item is dragged to the
+             * end of a {@link RecyclerView}.
+             * @param pixelsPastView
+             * @param scrollDuration
+             * @return
+             */
+            private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
+                final int maxScrollStep = mHelper.getHeight();
+                final int direction = (int) Math.signum(pixelsPastView);
+                final int absPastView = Math.abs(pixelsPastView);
+
+                // Calculate the ratio of how far out of the view the pointer currently resides to
+                // the entire height of the view.
+                final float outOfBoundsRatio = Math.min(
+                        1.0f, (float) absPastView / mHelper.getHeight());
+                // Interpolate this ratio and use it to compute the maximum scroll that should be
+                // possible for this step.
+                final float cappedScrollStep =
+                        direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
+
+                // Likewise, calculate the ratio of the time spent in the scroll to the limit.
+                final float timeRatio = Math.min(
+                        1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
+                // Interpolate this ratio and use it to compute the final number of pixels to
+                // scroll.
+                final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
+
+                // If the final number of pixels to scroll ends up being 0, the view should still
+                // scroll at least one pixel.
+                return numPixels != 0 ? numPixels : direction;
+            }
+
+            /**
+             * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
+             * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
+             * drags that are at the edge or barely past the edge of the view still cause sufficient
+             * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
+             * needed.
+             * @param ratio A ratio which is in the range [0, 1].
+             * @return A "smoothed" value, also in the range [0, 1].
+             */
+            private float smoothOutOfBoundsRatio(float ratio) {
+                return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
+            }
+
+            /**
+             * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
+             * and stays close to 0 for most input values except those very close to 1. This ensures
+             * that scrolls start out very slowly but speed up drastically after the scroll has been
+             * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
+             * but this could also be tweaked if needed.
+             * @param ratio A ratio which is in the range [0, 1].
+             * @return A "smoothed" value, also in the range [0, 1].
+             */
+            private float smoothTimeRatio(float ratio) {
+                return (float) Math.pow(ratio, 5);
+            }
+        };
+    }
+
+    /**
+     * Provides a band selection item model for views within a RecyclerView. This class queries the
+     * RecyclerView to determine where its items are placed; then, once band selection is underway,
+     * it alerts listeners of which items are covered by the selections.
+     */
+    public static final class BandSelectModel extends RecyclerView.OnScrollListener {
+
+        public static final int NOT_SET = -1;
+
+        // Enum values used to determine the corner at which the origin is located within the
+        private static final int UPPER = 0x00;
+        private static final int LOWER = 0x01;
+        private static final int LEFT = 0x00;
+        private static final int RIGHT = 0x02;
+        private static final int UPPER_LEFT = UPPER | LEFT;
+        private static final int UPPER_RIGHT = UPPER | RIGHT;
+        private static final int LOWER_LEFT = LOWER | LEFT;
+        private static final int LOWER_RIGHT = LOWER | RIGHT;
+
+        private final BandModelHelper mHelper;
+        private final List<OnSelectionChangedListener> mOnSelectionChangedListeners = new ArrayList<>();
+
+        // Map from the x-value of the left side of an item to an ordered list of metadata of all
+        // items whose x-values are the same. The list is ordered by the y-values of the items in
+        // the column. For example, if the first column of the view starts at an x-value of 5,
+        // mColumns.get(5) would return a list of all items in that column, with the top-most item
+        // first in the list and the bottom-most item last in the list.
+        private final Map<Integer, List<ItemData>> mColumns = new HashMap<>();
+
+        // List of limits along the x-axis. For example, if the view has two columns, this list will
+        // have two elements, each of which lists the lower- and upper-limits of the x-values of the
+        // view items. This list is sorted from furthest left to furthest right.
+        private final List<Limits> mXLimitsList = new ArrayList<>();
+
+        // Like mXLimitsList, but for y-coordinates. Note that this list only contains items which
+        // have been in the viewport. Thus, limits which exist in an area of the view to which the
+        // view has not scrolled are not present in the list.
+        private final List<Limits> mYLimitsList = new ArrayList<>();
+
+        // The adapter positions which have been recorded so far.
+        private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
+
+        // Array passed to registered OnSelectionChangedListeners. One array is created and reused
+        // throughout the lifetime of the object.
+        private final SparseBooleanArray mSelection = new SparseBooleanArray();
+
+        // The current pointer (in absolute positioning from the top of the view).
+        private Point mPointer = null;
+
+        // The bounds of the band selection.
+        private RelativePoint mRelativeOrigin;
+        private RelativePoint mRelativePointer;
+
+        private boolean mIsActive;
+
+        // Tracks where the band select originated from. This is used to determine where selections
+        // should expand from when Shift+click is used.
+        private int mPositionNearestOrigin = NOT_SET;
+
+        BandSelectModel(BandModelHelper helper) {
+            mHelper = helper;
+            mHelper.addOnScrollListener(this);
+        }
+
+        /**
+         * Stops listening to the view's scrolls. Call this function before discarding a
+         * BandSelecModel object to prevent memory leaks.
+         */
+        void stopListening() {
+            mHelper.removeOnScrollListener(this);
+        }
+
+        /**
+         * Start a band select operation at the given point.
+         * @param relativeOrigin The origin of the band select operation, relative to the viewport.
+         *     For example, if the view is scrolled to the bottom, the top-left of the viewport
+         *     would have a relative origin of (0, 0), even though its absolute point has a higher
+         *     y-value.
+         */
+        void startSelection(Point relativeOrigin) {
+            mIsActive = true;
+            mPointer = mHelper.createAbsolutePoint(relativeOrigin);
+
+            recordVisibleChildren();
+            mRelativeOrigin = new RelativePoint(mPointer);
+            mRelativePointer = new RelativePoint(mPointer);
+            computeCurrentSelection();
+            notifyListeners();
+        }
+
+        /**
+         * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
+         * opposite the origin.
+         * @param relativePointer The pointer (opposite of the origin) of the band select operation,
+         *     relative to the viewport. For example, if the view is scrolled to the bottom, the
+         *     top-left of the viewport would have a relative origin of (0, 0), even though its
+         *     absolute point has a higher y-value.
+         */
+        void resizeSelection(Point relativePointer) {
+            mPointer = mHelper.createAbsolutePoint(relativePointer);
+            updateModel();
+        }
+
+        /**
+         * Ends the band selection.
+         */
+        void endSelection() {
+            mIsActive = false;
+        }
+
+        /**
+         * @return The adapter position for the item nearest the origin corresponding to the latest
+         *         band select operation, or NOT_SET if the selection did not cover any items.
+         */
+        int getPositionNearestOrigin() {
+            return mPositionNearestOrigin;
+        }
+
+        @Override
+        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+            if (!mIsActive) {
+                return;
+            }
+
+            mPointer.x += dx;
+            mPointer.y += dy;
+            recordVisibleChildren();
+            updateModel();
+        }
+
+        /**
+         * Queries the view for all children and records their location metadata.
+         */
+        private void recordVisibleChildren() {
+            for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
+                int adapterPosition = mHelper.getAdapterPositionAt(i);
+                if (!mKnownPositions.get(adapterPosition)) {
+                    mKnownPositions.put(adapterPosition, true);
+                    recordItemData(
+                            mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
+                }
+            }
+        }
+
+        /**
+         * Updates the limits lists and column map with the given item metadata.
+         * @param absoluteChildRect The absolute rectangle for the child view being processed.
+         * @param adapterPosition The position of the child view being processed.
+         */
+        private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
+            if (mXLimitsList.size() != mHelper.getNumColumns()) {
+                // If not all x-limits have been recorded, record this one.
+                recordLimits(
+                        mXLimitsList, new Limits(absoluteChildRect.left, absoluteChildRect.right));
+            }
+
+            if (mYLimitsList.size() != mHelper.getNumRows()) {
+                // If not all y-limits have been recorded, record this one.
+                recordLimits(
+                        mYLimitsList, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
+            }
+
+            List<ItemData> columnList = mColumns.get(absoluteChildRect.left);
+            if (columnList == null) {
+                columnList = new ArrayList<ItemData>();
+                mColumns.put(absoluteChildRect.left, columnList);
+            }
+            ItemData itemData = new ItemData(adapterPosition, absoluteChildRect.top);
+            int index = Collections.binarySearch(columnList, itemData);
+            if (index < 0) {
+                columnList.add(~index, itemData);
+            }
+        }
+
+        /**
+         * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
+         * does not exist.
+         */
+        private void recordLimits(List<Limits> limitsList, Limits limits) {
+            int index = Collections.binarySearch(limitsList, limits);
+            if (index < 0) {
+                limitsList.add(~index, limits);
+            }
+        }
+
+        /**
+         * Handles a moved pointer; this function determines whether the pointer movement resulted
+         * in a selection change and, if it has, notifies listeners of this change.
+         */
+        private void updateModel() {
+            RelativePoint old = mRelativePointer;
+            mRelativePointer = new RelativePoint(mPointer);
+            if (old != null && mRelativePointer.equals(old)) {
+                return;
+            }
+
+            computeCurrentSelection();
+            notifyListeners();
+        }
+
+        /**
+         * Computes the currently-selected items.
+         */
+        private void computeCurrentSelection() {
+            if (areItemsCoveredBySelection(mRelativePointer, mRelativeOrigin)) {
+                updateSelection(computeBounds());
+            } else {
+                mSelection.clear();
+                mPositionNearestOrigin = NOT_SET;
+            }
+        }
+
+        /**
+         * Notifies all listeners of a selection change. Note that this function simply passes
+         * mSelection, so computeCurrentSelection() should be called before this
+         * function.
+         */
+        private void notifyListeners() {
+            for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
+                listener.onSelectionChanged(mSelection);
+            }
+        }
+
+        /**
+         * @param rect Rectangle including all covered items.
+         */
+        private void updateSelection(Rect rect) {
+            int columnStartIndex =
+                    Collections.binarySearch(mXLimitsList, new Limits(rect.left, rect.left));
+            checkState(columnStartIndex >= 0);
+            int columnEndIndex = columnStartIndex;
+
+            for (int i = columnStartIndex;
+                    i < mXLimitsList.size() && mXLimitsList.get(i).lowerLimit <= rect.right; i++) {
+                columnEndIndex = i;
+            }
+
+            List<ItemData> firstColumn =
+                    mColumns.get(mXLimitsList.get(columnStartIndex).lowerLimit);
+            int rowStartIndex = Collections.binarySearch(firstColumn, new ItemData(0, rect.top));
+            if (rowStartIndex < 0) {
+                mPositionNearestOrigin = NOT_SET;
+                return;
+            }
+
+            int rowEndIndex = rowStartIndex;
+            for (int i = rowStartIndex;
+                    i < firstColumn.size() && firstColumn.get(i).offset <= rect.bottom; i++) {
+                rowEndIndex = i;
+            }
+
+            updateSelection(columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex);
+        }
+
+        /**
+         * Computes the selection given the previously-computed start- and end-indices for each
+         * row and column.
+         */
+        private void updateSelection(
+                int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+            mSelection.clear();
+            for (int column = columnStartIndex; column <= columnEndIndex; column++) {
+                List<ItemData> items = mColumns.get(mXLimitsList.get(column).lowerLimit);
+                for (int row = rowStartIndex; row <= rowEndIndex; row++) {
+                    int position = items.get(row).position;
+                    mSelection.append(position, true);
+                    if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
+                            row, rowStartIndex, rowEndIndex)) {
+                        // If this is the position nearest the origin, record it now so that it
+                        // can be returned by endSelection() later.
+                        mPositionNearestOrigin = position;
+                    }
+                }
+            }
+        }
+
+        /**
+         * @return Returns true if the position is the nearest to the origin, or, in the case of the
+         *     lower-right corner, whether it is possible that the position is the nearest to the
+         *     origin. See comment below for reasoning for this special case.
+         */
+        private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
+                int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
+            int corner = computeCornerNearestOrigin();
+            switch (corner) {
+                case UPPER_LEFT:
+                    return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
+                case UPPER_RIGHT:
+                    return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
+                case LOWER_LEFT:
+                    return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
+                case LOWER_RIGHT:
+                    // Note that in some cases, the last row will not have as many items as there
+                    // are columns (e.g., if there are 4 items and 3 columns, the second row will
+                    // only have one item in the first column). This function is invoked for each
+                    // position from left to right, so return true for any position in the bottom
+                    // row and only the right-most position in the bottom row will be recorded.
+                    return rowIndex == rowEndIndex;
+                default:
+                    throw new RuntimeException("Invalid corner type.");
+            }
+        }
+
+        /**
+         * Listener for changes in which items have been band selected.
+         */
+        static interface OnSelectionChangedListener {
+            public void onSelectionChanged(SparseBooleanArray updatedSelection);
+        }
+
+        void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
+            mOnSelectionChangedListeners.add(listener);
+        }
+
+        void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
+            mOnSelectionChangedListeners.remove(listener);
+        }
+
+        /**
+         * Metadata for an item in the view, consisting of the adapter position and the offset from the
+         * top of the view (in pixels). Stored in the mColumns map to model the item grid.
+         */
+        private class ItemData implements Comparable<ItemData> {
+            int position;
+            int offset;
+
+            ItemData(int position, int offset) {
+                this.position = position;
+                this.offset = offset;
+            }
+
+            @Override
+            public int compareTo(ItemData other) {
+                // The list of columns is sorted via the offset from the top, so PositionMetadata
+                // objects with lower y-values are befor those with higher y-values.
+                return offset - other.offset;
+            }
+        }
+
+        /**
+         * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
+         * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
+         * of item columns and the top- and bottom sides of item rows so that it can be determined
+         * whether the pointer is located within the bounds of an item.
+         */
+        private class Limits implements Comparable<Limits> {
+            int lowerLimit;
+            int upperLimit;
+
+            Limits(int lowerLimit, int upperLimit) {
+                this.lowerLimit = lowerLimit;
+                this.upperLimit = upperLimit;
+            }
+
+            @Override
+            public int compareTo(Limits other) {
+                return lowerLimit - other.lowerLimit;
+            }
+
+            @Override
+            public boolean equals(Object other) {
+                if (!(other instanceof Limits)) {
+                    return false;
+                }
+
+                return ((Limits) other).lowerLimit == lowerLimit &&
+                        ((Limits) other).upperLimit == upperLimit;
+            }
+        }
+
+        /**
+         * The location of a coordinate relative to items. This class represents a general area of the
+         * view as it relates to band selection rather than an explicit point. For example, two
+         * different points within an item are considered to have the same "location" because band
+         * selection originating within the item would select the same items no matter which point
+         * was used. Same goes for points between items as well as those at the very beginning or end
+         * of the view.
+         *
+         * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
+         * advantage of tying the value to the Limits of items along that axis. This allows easy
+         * selection of items within those Limits as opposed to a search through every item to see if a
+         * given coordinate value falls within those Limits.
+         */
+        private class RelativeCoordinate
+                implements Comparable<RelativeCoordinate> {
+            /**
+             * Location describing points after the last known item.
+             */
+            static final int AFTER_LAST_ITEM = 0;
+
+            /**
+             * Location describing points before the first known item.
+             */
+            static final int BEFORE_FIRST_ITEM = 1;
+
+            /**
+             * Location describing points between two items.
+             */
+            static final int BETWEEN_TWO_ITEMS = 2;
+
+            /**
+             * Location describing points within the limits of one item.
+             */
+            static final int WITHIN_LIMITS = 3;
+
+            /**
+             * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
+             * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
+             */
+            final int type;
+
+            /**
+             * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
+             * BETWEEN_TWO_ITEMS.
+             */
+            Limits limitsBeforeCoordinate;
+
+            /**
+             * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
+             */
+            Limits limitsAfterCoordinate;
+
+            // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
+            Limits mFirstKnownItem;
+            // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
+            Limits mLastKnownItem;
+
+            /**
+             * @param limitsList The sorted limits list for the coordinate type. If this
+             *     CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
+             *     mYLimitsList should be pased.
+             * @param value The coordinate value.
+             */
+            RelativeCoordinate(List<Limits> limitsList, int value) {
+                Limits dummyLimits = new Limits(value, value);
+                int index = Collections.binarySearch(limitsList, dummyLimits);
+
+                if (index >= 0) {
+                    this.type = WITHIN_LIMITS;
+                    this.limitsBeforeCoordinate = limitsList.get(index);
+                } else if (~index == 0) {
+                    this.type = BEFORE_FIRST_ITEM;
+                    this.mFirstKnownItem = limitsList.get(0);
+                } else if (~index == limitsList.size()) {
+                    Limits lastLimits = limitsList.get(limitsList.size() - 1);
+                    if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
+                        this.type = WITHIN_LIMITS;
+                        this.limitsBeforeCoordinate = lastLimits;
+                    } else {
+                        this.type = AFTER_LAST_ITEM;
+                        this.mLastKnownItem = lastLimits;
+                    }
+                } else {
+                    Limits limitsBeforeIndex = limitsList.get(~index - 1);
+                    if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
+                        this.type = WITHIN_LIMITS;
+                        this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+                    } else {
+                        this.type = BETWEEN_TWO_ITEMS;
+                        this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+                        this.limitsAfterCoordinate = limitsList.get(~index);
+                    }
+                }
+            }
+
+            int toComparisonValue() {
+                if (type == BEFORE_FIRST_ITEM) {
+                    return mFirstKnownItem.lowerLimit - 1;
+                } else if (type == AFTER_LAST_ITEM) {
+                    return mLastKnownItem.upperLimit + 1;
+                } else if (type == BETWEEN_TWO_ITEMS) {
+                    return limitsBeforeCoordinate.upperLimit + 1;
+                } else {
+                    return limitsBeforeCoordinate.lowerLimit;
+                }
+            }
+
+            @Override
+            public boolean equals(Object other) {
+                if (!(other instanceof RelativeCoordinate)) {
+                    return false;
+                }
+
+                RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
+                return toComparisonValue() == otherCoordinate.toComparisonValue();
+            }
+
+            @Override
+            public int compareTo(RelativeCoordinate other) {
+                return toComparisonValue() - other.toComparisonValue();
+            }
+        }
+
+        /**
+         * The location of a point relative to the Limits of nearby items; consists of both an x- and
+         * y-RelativeCoordinateLocation.
+         */
+        private class RelativePoint {
+            final RelativeCoordinate xLocation;
+            final RelativeCoordinate yLocation;
+
+            RelativePoint(Point point) {
+                this.xLocation = new RelativeCoordinate(mXLimitsList, point.x);
+                this.yLocation = new RelativeCoordinate(mYLimitsList, point.y);
+            }
+
+            @Override
+            public boolean equals(Object other) {
+                if (!(other instanceof RelativePoint)) {
+                    return false;
+                }
+
+                RelativePoint otherPoint = (RelativePoint) other;
+                return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
+            }
+        }
+
+        /**
+         * Generates a rectangle which contains the items selected by the pointer and origin.
+         * @return The rectangle, or null if no items were selected.
+         */
+        private Rect computeBounds() {
+            Rect rect = new Rect();
+            rect.left = getCoordinateValue(
+                    min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
+                    mXLimitsList,
+                    true);
+            rect.right = getCoordinateValue(
+                    max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
+                    mXLimitsList,
+                    false);
+            rect.top = getCoordinateValue(
+                    min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
+                    mYLimitsList,
+                    true);
+            rect.bottom = getCoordinateValue(
+                    max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
+                    mYLimitsList,
+                    false);
+            return rect;
+        }
+
+        /**
+         * Computes the corner of the selection nearest the origin.
+         * @return
+         */
+        private int computeCornerNearestOrigin() {
+            int cornerValue = 0;
+
+            if (mRelativeOrigin.yLocation ==
+                    min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
+                cornerValue |= UPPER;
+            } else {
+                cornerValue |= LOWER;
+            }
+
+            if (mRelativeOrigin.xLocation ==
+                    min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
+                cornerValue |= LEFT;
+            } else {
+                cornerValue |= RIGHT;
+            }
+
+            return cornerValue;
+        }
+
+        private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
+            return first.compareTo(second) < 0 ? first : second;
+        }
+
+        private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
+            return first.compareTo(second) > 0 ? first : second;
+        }
+
+        /**
+         * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
+         *     coordinate.
+         */
+        private int getCoordinateValue(RelativeCoordinate coordinate,
+                List<Limits> limitsList, boolean isStartOfRange) {
+            switch (coordinate.type) {
+                case RelativeCoordinate.BEFORE_FIRST_ITEM:
+                    return limitsList.get(0).lowerLimit;
+                case RelativeCoordinate.AFTER_LAST_ITEM:
+                    return limitsList.get(limitsList.size() - 1).upperLimit;
+                case RelativeCoordinate.BETWEEN_TWO_ITEMS:
+                    if (isStartOfRange) {
+                        return coordinate.limitsAfterCoordinate.lowerLimit;
+                    } else {
+                        return coordinate.limitsBeforeCoordinate.upperLimit;
+                    }
+                case RelativeCoordinate.WITHIN_LIMITS:
+                    return coordinate.limitsBeforeCoordinate.lowerLimit;
+            }
+
+            throw new RuntimeException("Invalid coordinate value.");
+        }
+
+        private boolean areItemsCoveredBySelection(
+                RelativePoint first, RelativePoint second) {
+            return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
+                    doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
+        }
+
+        private boolean doesCoordinateLocationCoverItems(
+                RelativeCoordinate pointerCoordinate,
+                RelativeCoordinate originCoordinate) {
+            if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
+                    originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
+                return false;
+            }
+
+            if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
+                    originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
+                return false;
+            }
+
+            if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
+                    originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
+                    pointerCoordinate.limitsBeforeCoordinate.equals(
+                            originCoordinate.limitsBeforeCoordinate) &&
+                    pointerCoordinate.limitsAfterCoordinate.equals(
+                            originCoordinate.limitsAfterCoordinate)) {
+                return false;
+            }
+
+            return true;
+        }
+    }
 }
@@ -18,7 +18,7 @@ package com.android.documentsui;
 
 import static org.junit.Assert.*;
 
-import com.android.documentsui.BandSelectMatrix;
+import com.android.documentsui.MultiSelectManager.BandSelectModel;
 
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -28,13 +28,13 @@ import android.util.SparseBooleanArray;
 import org.junit.After;
 import org.junit.Test;
 
-public class BandSelectMatrixTest {
+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 BandSelectMatrix matrix;
+    private static BandSelectModel model;
     private static TestHelper helper;
     private static SparseBooleanArray lastSelection;
     private static int viewWidth;
@@ -42,8 +42,8 @@ public class BandSelectMatrixTest {
     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);
-        matrix = new BandSelectMatrix(helper);
-        matrix.addOnSelectionChangedListener(new BandSelectMatrix.OnSelectionChangedListener() {
+        model = new BandSelectModel(helper);
+        model.addOnSelectionChangedListener(new BandSelectModel.OnSelectionChangedListener() {
 
             @Override
             public void onSelectionChanged(SparseBooleanArray updatedSelection) {
@@ -54,7 +54,7 @@ public class BandSelectMatrixTest {
 
     @After
     public void tearDown() {
-        matrix = null;
+        model = null;
         helper = null;
         lastSelection = null;
     }
@@ -62,111 +62,120 @@ public class BandSelectMatrixTest {
     @Test
     public void testSelectionLeftOfItems() {
         setUp(20, 5);
-        matrix.startSelection(new Point(0, 10));
-        matrix.resizeSelection(new Point(1, 11));
+        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);
-        matrix.startSelection(new Point(viewWidth - 1, 10));
-        matrix.resizeSelection(new Point(viewWidth - 2, 11));
+        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);
-        matrix.startSelection(new Point(10, 0));
-        matrix.resizeSelection(new Point(11, 1));
+        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);
-        matrix.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
-        matrix.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
+        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);
-        matrix.startSelection(new Point(106, 0));
-        matrix.resizeSelection(new Point(107, 200));
+        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);
-        matrix.startSelection(new Point(0, 105));
-        matrix.resizeSelection(new Point(200, 106));
+        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);
-        matrix.startSelection(new Point(0, 0));
-        matrix.resizeSelection(new Point(5, 5));
+        model.startSelection(new Point(0, 0));
+        model.resizeSelection(new Point(5, 5));
         assertSelected(new int[] {0});
-        matrix.resizeSelection(new Point(109, 109));
+        model.resizeSelection(new Point(109, 109));
         assertSelected(new int[] {0});
-        matrix.resizeSelection(new Point(110, 109));
+        model.resizeSelection(new Point(110, 109));
         assertSelected(new int[] {0, 1});
-        matrix.resizeSelection(new Point(110, 110));
+        model.resizeSelection(new Point(110, 110));
         assertSelected(new int[] {0, 1, 4, 5});
-        matrix.resizeSelection(new Point(214, 214));
+        model.resizeSelection(new Point(214, 214));
         assertSelected(new int[] {0, 1, 4, 5});
-        matrix.resizeSelection(new Point(215, 214));
+        model.resizeSelection(new Point(215, 214));
         assertSelected(new int[] {0, 1, 2, 4, 5, 6});
-        matrix.resizeSelection(new Point(214, 214));
+        model.resizeSelection(new Point(214, 214));
         assertSelected(new int[] {0, 1, 4, 5});
-        matrix.resizeSelection(new Point(110, 110));
+        model.resizeSelection(new Point(110, 110));
         assertSelected(new int[] {0, 1, 4, 5});
-        matrix.resizeSelection(new Point(110, 109));
+        model.resizeSelection(new Point(110, 109));
         assertSelected(new int[] {0, 1});
-        matrix.resizeSelection(new Point(109, 109));
+        model.resizeSelection(new Point(109, 109));
         assertSelected(new int[] {0});
-        matrix.resizeSelection(new Point(5, 5));
+        model.resizeSelection(new Point(5, 5));
         assertSelected(new int[] {0});
-        matrix.resizeSelection(new Point(0, 0));
+        model.resizeSelection(new Point(0, 0));
         assertSelected(new int[0]);
+        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
     }
 
     @Test
     public void testSelectionMovingAroundOrigin() {
         setUp(16, 4);
-        matrix.startSelection(new Point(210, 210));
-        matrix.resizeSelection(new Point(viewWidth - 1, 0));
+        model.startSelection(new Point(210, 210));
+        model.resizeSelection(new Point(viewWidth - 1, 0));
         assertSelected(new int[] {2, 3, 6, 7});
-        matrix.resizeSelection(new Point(0, 0));
+        model.resizeSelection(new Point(0, 0));
         assertSelected(new int[] {0, 1, 4, 5});
-        matrix.resizeSelection(new Point(0, 420));
+        model.resizeSelection(new Point(0, 420));
         assertSelected(new int[] {8, 9, 12, 13});
-        matrix.resizeSelection(new Point(viewWidth - 1, 420));
+        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);
-        matrix.startSelection(new Point(0, 0));
-        matrix.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+        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});
-        matrix.resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
+        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});
-        matrix.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+        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) {
@@ -179,10 +188,10 @@ public class BandSelectMatrixTest {
     private static void scroll(int dy) {
         assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight());
         helper.verticalOffset += dy;
-        matrix.onScrolled(null, 0, dy);
+        model.onScrolled(null, 0, dy);
     }
 
-    private static final class TestHelper implements BandSelectMatrix.RecyclerViewHelper {
+    private static final class TestHelper implements MultiSelectManager.BandModelHelper {
 
         public int horizontalOffset = 0;
         public int verticalOffset = 0;
index 03ad3d4..b82251c 100644 (file)
@@ -19,12 +19,12 @@ package com.android.documentsui;
 import static org.junit.Assert.*;
 
 import android.support.v7.widget.RecyclerView;
+import android.util.SparseBooleanArray;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.documentsui.MultiSelectManager.RecyclerViewHelper;
 import com.android.documentsui.MultiSelectManager.Selection;
 
 import org.junit.Before;
@@ -79,7 +79,7 @@ public class MultiSelectManagerTest {
 
     @Test
     public void mouseClick_NoPosition_ClearsSelection() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         click(11);
         click(RecyclerView.NO_POSITION);
         assertSelection();
@@ -95,27 +95,27 @@ public class MultiSelectManagerTest {
 
     @Test
     public void longPress_StartsSelectionMode() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         assertSelection(7);
     }
 
     @Test
     public void longPress_SecondPressExtendsSelection() {
-        mManager.onLongPress(7);
-        mManager.onLongPress(99);
+        mManager.onLongPress(7, 0);
+        mManager.onLongPress(99, 0);
         assertSelection(7, 99);
     }
 
     @Test
     public void singleTapUp_UnselectsSelectedItem() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         tap(7);
         assertSelection();
     }
 
     @Test
     public void singleTapUp_NoPosition_ClearsSelection() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         tap(11);
         tap(RecyclerView.NO_POSITION);
         assertSelection();
@@ -123,7 +123,7 @@ public class MultiSelectManagerTest {
 
     @Test
     public void singleTapUp_ExtendsSelection() {
-        mManager.onLongPress(99);
+        mManager.onLongPress(99, 0);
         tap(7);
         tap(13);
         tap(129899);
@@ -132,21 +132,21 @@ public class MultiSelectManagerTest {
 
     @Test
     public void singleTapUp_ShiftCreatesRangeSelection() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         shiftTap(17);
         assertRangeSelection(7, 17);
     }
 
     @Test
     public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() {
-        mManager.onLongPress(17);
+        mManager.onLongPress(17, 0);
         shiftTap(7);
         assertRangeSelection(7, 17);
     }
 
     @Test
     public void singleTapUp_SecondShiftClickExtendsSelection() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         shiftTap(11);
         shiftTap(17);
         assertRangeSelection(7, 17);
@@ -154,7 +154,7 @@ public class MultiSelectManagerTest {
 
     @Test
     public void singleTapUp_MultipleContiguousRangesSelected() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         shiftTap(11);
         tap(20);
         shiftTap(25);
@@ -165,7 +165,7 @@ public class MultiSelectManagerTest {
 
     @Test
     public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         shiftTap(17);
         shiftTap(10);
         assertRangeSelection(7, 10);
@@ -173,7 +173,7 @@ public class MultiSelectManagerTest {
 
     @Test
     public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick_Backwards() {
-        mManager.onLongPress(17);
+        mManager.onLongPress(17, 0);
         shiftTap(7);
         shiftTap(14);
         assertRangeSelection(14, 17);
@@ -182,7 +182,7 @@ public class MultiSelectManagerTest {
 
     @Test
     public void singleTapUp_ShiftReversesSelectionDirection() {
-        mManager.onLongPress(7);
+        mManager.onLongPress(7, 0);
         shiftTap(17);
         shiftTap(0);
         assertRangeSelection(0, 7);
@@ -206,6 +206,36 @@ public class MultiSelectManagerTest {
         assertSelection(20);
     }
 
+    @Test
+    public void provisionaSelection() {
+        Selection s = mManager.getSelection();
+        assertSelection();
+
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(1, true);
+        provisional.append(2, true);
+        s.setProvisionalSelection(provisional);
+        assertSelection(1, 2);
+
+        provisional.delete(1);
+        provisional.append(3, true);
+        s.setProvisionalSelection(provisional);
+        assertSelection(2, 3);
+
+        s.applyProvisionalSelection();
+        assertSelection(2, 3);
+
+        provisional.clear();
+        provisional.append(3, true);
+        provisional.append(4, true);
+        s.setProvisionalSelection(provisional);
+        assertSelection(2, 3, 4);
+
+        provisional.delete(3);
+        s.setProvisionalSelection(provisional);
+        assertSelection(2, 3, 4);
+    }
+
     private void tap(int position) {
         mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE);
     }
@@ -252,7 +282,8 @@ public class MultiSelectManagerTest {
         assertEquals(selection.toString(), expected, selection.size());
     }
 
-    private static final class EventHelper implements RecyclerViewHelper {
+    private static final class EventHelper implements MultiSelectManager.MultiSelectHelper {
+
         @Override
         public int findEventPosition(MotionEvent e) {
             throw new UnsupportedOperationException();
index 0553270..d90130f 100644 (file)
@@ -22,7 +22,7 @@ import org.junit.runners.Suite.SuiteClasses;
 
 @RunWith(Suite.class)
 @SuiteClasses({
-        BandSelectMatrixTest.class,
+        BandSelectModelTest.class,
         MultiSelectManager_SelectionTest.class,
         MultiSelectManagerTest.class
 })