blob: 1cb2900a546ee5768767815e276bf74f23fb897e [file] [log] [blame]
/*
* 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.selection;
import static com.android.documentsui.selection.GridModel.NOT_SET;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import com.android.documentsui.selection.BandSelectionHelper.BandHost;
import com.android.documentsui.selection.testing.SelectionPredicates;
import com.android.documentsui.selection.testing.TestAdapter;
import com.android.documentsui.selection.testing.TestStableIdProvider;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class GridModelTest {
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 GridModel mModel;
private TestHost mHost;
private TestAdapter mAdapter;
private Set<String> mLastSelection;
private int mViewWidth;
// TLDR: Don't call model.{start|resize}Selection; use the local #startSelection and
// #resizeSelection methods instead.
//
// The reason for this is that selection is stateful and involves operations that take the
// current UI state (e.g scrolling) into account. This test maintains its own copy of the
// selection bounds as control data for verifying selections. Keep this data in sync by calling
// #startSelection and
// #resizeSelection.
private Point mSelectionOrigin;
private Point mSelectionPoint;
@After
public void tearDown() {
mModel = null;
mHost = null;
mLastSelection = null;
}
@Test
public void testSelectionLeftOfItems() {
initData(20, 5);
startSelection(new Point(0, 10));
resizeSelection(new Point(1, 11));
assertNoSelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testSelectionRightOfItems() {
initData(20, 4);
startSelection(new Point(mViewWidth - 1, 10));
resizeSelection(new Point(mViewWidth - 2, 11));
assertNoSelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testSelectionAboveItems() {
initData(20, 4);
startSelection(new Point(10, 0));
resizeSelection(new Point(11, 1));
assertNoSelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testSelectionBelowItems() {
initData(5, 4);
startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
assertNoSelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testVerticalSelectionBetweenItems() {
initData(20, 4);
startSelection(new Point(106, 0));
resizeSelection(new Point(107, 200));
assertNoSelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testHorizontalSelectionBetweenItems() {
initData(20, 4);
startSelection(new Point(0, 105));
resizeSelection(new Point(200, 106));
assertNoSelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testGrowingAndShrinkingSelection() {
initData(20, 4);
startSelection(new Point(0, 0));
resizeSelection(new Point(5, 5));
verifySelection();
resizeSelection(new Point(109, 109));
verifySelection();
resizeSelection(new Point(110, 109));
verifySelection();
resizeSelection(new Point(110, 110));
verifySelection();
resizeSelection(new Point(214, 214));
verifySelection();
resizeSelection(new Point(215, 214));
verifySelection();
resizeSelection(new Point(214, 214));
verifySelection();
resizeSelection(new Point(110, 110));
verifySelection();
resizeSelection(new Point(110, 109));
verifySelection();
resizeSelection(new Point(109, 109));
verifySelection();
resizeSelection(new Point(5, 5));
verifySelection();
resizeSelection(new Point(0, 0));
verifySelection();
assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
}
@Test
public void testSelectionMovingAroundOrigin() {
initData(16, 4);
startSelection(new Point(210, 210));
resizeSelection(new Point(mViewWidth - 1, 0));
verifySelection();
resizeSelection(new Point(0, 0));
verifySelection();
resizeSelection(new Point(0, 420));
verifySelection();
resizeSelection(new Point(mViewWidth - 1, 420));
verifySelection();
// This is manually figured and will need to be adjusted if the separator position is
// changed.
assertEquals(7, mModel.getPositionNearestOrigin());
}
@Test
public void testScrollingBandSelect() {
initData(40, 4);
startSelection(new Point(0, 0));
resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
verifySelection();
scroll(CHILD_VIEW_EDGE_PX);
verifySelection();
resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
verifySelection();
scroll(CHILD_VIEW_EDGE_PX);
verifySelection();
scroll(-2 * CHILD_VIEW_EDGE_PX);
verifySelection();
resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
verifySelection();
assertEquals(0, mModel.getPositionNearestOrigin());
}
private void initData(final int numChildren, int numColumns) {
mHost = new TestHost(numChildren, numColumns);
mAdapter = new TestAdapter() {
@Override
public String getStableId(int position) {
return Integer.toString(position);
}
@Override
public int getItemCount() {
return numChildren;
}
};
mViewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
mModel = new GridModel(
mHost,
new TestStableIdProvider(mAdapter),
SelectionPredicates.CAN_SET_ANYTHING);
mModel.addOnSelectionChangedListener(
new GridModel.SelectionObserver() {
@Override
public void onSelectionChanged(Set<String> updatedSelection) {
mLastSelection = updatedSelection;
}
});
}
/** Returns the current selection area as a Rect. */
private Rect getSelectionArea() {
// Construct a rect from the two selection points.
Rect selectionArea = new Rect(
mSelectionOrigin.x, mSelectionOrigin.y, mSelectionOrigin.x, mSelectionOrigin.y);
selectionArea.union(mSelectionPoint.x, mSelectionPoint.y);
// Rect intersection tests are exclusive of bounds, while the MSM's selection code is
// inclusive. Expand the rect by 1 pixel in all directions to account for this.
selectionArea.inset(-1, -1);
return selectionArea;
}
/** Asserts that the selection is currently empty. */
private void assertNoSelection() {
assertEquals("Unexpected items " + mLastSelection + " in selection " + getSelectionArea(),
0, mLastSelection.size());
}
/** Verifies the selection using actual bbox checks. */
private void verifySelection() {
Rect selectionArea = getSelectionArea();
for (TestHost.Item item: mHost.items) {
if (Rect.intersects(selectionArea, item.rect)) {
assertTrue("Expected item " + item + " was not in selection " + selectionArea,
mLastSelection.contains(item.name));
} else {
assertFalse("Unexpected item " + item + " in selection" + selectionArea,
mLastSelection.contains(item.name));
}
}
}
private void startSelection(Point p) {
mModel.startCapturing(p);
mSelectionOrigin = mHost.createAbsolutePoint(p);
}
private void resizeSelection(Point p) {
mModel.resizeSelection(p);
mSelectionPoint = mHost.createAbsolutePoint(p);
}
private void scroll(int dy) {
assertTrue(mHost.verticalOffset + VIEWPORT_HEIGHT + dy <= mHost.getTotalHeight());
mHost.verticalOffset += dy;
// Correct the cached selection point as well.
mSelectionPoint.y += dy;
mHost.mScrollListener.onScrolled(null, 0, dy);
}
private static final class TestHost extends BandHost {
private final int mNumColumns;
private final int mNumRows;
private final int mNumChildren;
private final int mSeparatorPosition;
public int horizontalOffset = 0;
public int verticalOffset = 0;
private List<Item> items = new ArrayList<>();
// Installed by GridModel on construction.
private @Nullable OnScrollListener mScrollListener;
public TestHost(int numChildren, int numColumns) {
mNumChildren = numChildren;
mNumColumns = numColumns;
mSeparatorPosition = mNumColumns + 1;
mNumRows = setupGrid();
}
private int setupGrid() {
// Split the input set into folders and documents. Do this such that there is a
// partially-populated row in the middle of the grid, to test corner cases in layout
// code.
int y = VIEW_PADDING_PX;
int i = 0;
int numRows = 0;
while (i < mNumChildren) {
int top = y;
int height = CHILD_VIEW_EDGE_PX;
int width = CHILD_VIEW_EDGE_PX;
for (int j = 0; j < mNumColumns && i < mNumChildren; j++) {
int left = VIEW_PADDING_PX + (j * (width + VIEW_PADDING_PX));
items.add(new Item(
Integer.toString(i),
new Rect(
left,
top,
left + width - 1,
top + height - 1)));
// Create a partially populated row at the separator position.
if (++i == mSeparatorPosition) {
break;
}
}
y += height + VIEW_PADDING_PX;
numRows++;
}
return numRows;
}
private int getTotalHeight() {
return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1);
}
private int getFirstVisibleRowIndex() {
return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
}
private int getLastVisibleRowIndex() {
int lastVisibleRowUncapped =
(VIEWPORT_HEIGHT + verticalOffset - 1) / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
return Math.min(lastVisibleRowUncapped, mNumRows - 1);
}
private int getNumItemsInRow(int index) {
assertTrue(index >= 0 && index < mNumRows);
int mod = mSeparatorPosition % mNumColumns;
if (index == (mSeparatorPosition / mNumColumns)) {
// The row containing the separator may be incomplete
return mod > 0 ? mod : mNumColumns;
}
// Account for the partial separator row in the final row tally.
if (index == mNumRows - 1) {
// The last row may be incomplete
int finalRowCount = (mNumChildren - mod) % mNumColumns;
return finalRowCount > 0 ? finalRowCount : mNumColumns;
}
return mNumColumns;
}
@Override
public void addOnScrollListener(OnScrollListener listener) {
mScrollListener = listener;
}
@Override
public void removeOnScrollListener(OnScrollListener listener) {}
@Override
public Point createAbsolutePoint(Point relativePoint) {
return new Point(
relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset);
}
@Override
public int getVisibleChildCount() {
int childCount = 0;
for (int i = getFirstVisibleRowIndex(); i <= getLastVisibleRowIndex(); i++) {
childCount += getNumItemsInRow(i);
}
return childCount;
}
@Override
public int getAdapterPositionAt(int index) {
// Account for partial rows by actually tallying up the items in hidden rows.
int hiddenCount = 0;
for (int i = 0; i < getFirstVisibleRowIndex(); i++) {
hiddenCount += getNumItemsInRow(i);
}
return index + hiddenCount;
}
@Override
public Rect getAbsoluteRectForChildViewAt(int index) {
int adapterPosition = getAdapterPositionAt(index);
return items.get(adapterPosition).rect;
}
@Override
public int getChildCount() {
return mNumChildren;
}
@Override
public int getColumnCount() {
return mNumColumns;
}
@Override
public void showBand(Rect rect) {
throw new UnsupportedOperationException();
}
@Override
public void hideBand() {
throw new UnsupportedOperationException();
}
@Override
public void scrollBy(int dy) {
throw new UnsupportedOperationException();
}
@Override
public int getHeight() {
throw new UnsupportedOperationException();
}
@Override
public void invalidateView() {
throw new UnsupportedOperationException();
}
@Override
public void runAtNextFrame(Runnable r) {
throw new UnsupportedOperationException();
}
@Override
public void removeCallback(Runnable r) {
throw new UnsupportedOperationException();
}
@Override
public boolean hasView(int adapterPosition) {
return true;
}
public static final class Item {
public String name;
public Rect rect;
public Item(String n, Rect r) {
name = n;
rect = r;
}
@Override
public String toString() {
return name + ": " + rect;
}
}
}
}