1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.documentsui.selection; 18 19 import static com.android.documentsui.selection.GridModel.NOT_SET; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assert.assertTrue; 23 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 import android.support.test.filters.SmallTest; 27 import android.support.test.runner.AndroidJUnit4; 28 import android.support.v7.widget.RecyclerView.OnScrollListener; 29 30 import com.android.documentsui.selection.BandSelectionHelper.BandHost; 31 import com.android.documentsui.selection.testing.SelectionPredicates; 32 import com.android.documentsui.selection.testing.TestAdapter; 33 import com.android.documentsui.selection.testing.TestStableIdProvider; 34 35 import org.junit.After; 36 import org.junit.Test; 37 import org.junit.runner.RunWith; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Set; 42 43 import javax.annotation.Nullable; 44 45 @RunWith(AndroidJUnit4.class) 46 @SmallTest 47 public class GridModelTest { 48 49 private static final int VIEW_PADDING_PX = 5; 50 private static final int CHILD_VIEW_EDGE_PX = 100; 51 private static final int VIEWPORT_HEIGHT = 500; 52 53 private GridModel mModel; 54 private TestHost mHost; 55 private TestAdapter mAdapter; 56 private Set<String> mLastSelection; 57 private int mViewWidth; 58 59 // TLDR: Don't call model.{start|resize}Selection; use the local #startSelection and 60 // #resizeSelection methods instead. 61 // 62 // The reason for this is that selection is stateful and involves operations that take the 63 // current UI state (e.g scrolling) into account. This test maintains its own copy of the 64 // selection bounds as control data for verifying selections. Keep this data in sync by calling 65 // #startSelection and 66 // #resizeSelection. 67 private Point mSelectionOrigin; 68 private Point mSelectionPoint; 69 70 @After 71 public void tearDown() { 72 mModel = null; 73 mHost = null; 74 mLastSelection = null; 75 } 76 77 @Test 78 public void testSelectionLeftOfItems() { 79 initData(20, 5); 80 startSelection(new Point(0, 10)); 81 resizeSelection(new Point(1, 11)); 82 assertNoSelection(); 83 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 84 } 85 86 @Test 87 public void testSelectionRightOfItems() { 88 initData(20, 4); 89 startSelection(new Point(mViewWidth - 1, 10)); 90 resizeSelection(new Point(mViewWidth - 2, 11)); 91 assertNoSelection(); 92 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 93 } 94 95 @Test 96 public void testSelectionAboveItems() { 97 initData(20, 4); 98 startSelection(new Point(10, 0)); 99 resizeSelection(new Point(11, 1)); 100 assertNoSelection(); 101 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 102 } 103 104 @Test 105 public void testSelectionBelowItems() { 106 initData(5, 4); 107 startSelection(new Point(10, VIEWPORT_HEIGHT - 1)); 108 resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2)); 109 assertNoSelection(); 110 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 111 } 112 113 @Test 114 public void testVerticalSelectionBetweenItems() { 115 initData(20, 4); 116 startSelection(new Point(106, 0)); 117 resizeSelection(new Point(107, 200)); 118 assertNoSelection(); 119 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 120 } 121 122 @Test 123 public void testHorizontalSelectionBetweenItems() { 124 initData(20, 4); 125 startSelection(new Point(0, 105)); 126 resizeSelection(new Point(200, 106)); 127 assertNoSelection(); 128 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 129 } 130 131 @Test 132 public void testGrowingAndShrinkingSelection() { 133 initData(20, 4); 134 startSelection(new Point(0, 0)); 135 136 resizeSelection(new Point(5, 5)); 137 verifySelection(); 138 139 resizeSelection(new Point(109, 109)); 140 verifySelection(); 141 142 resizeSelection(new Point(110, 109)); 143 verifySelection(); 144 145 resizeSelection(new Point(110, 110)); 146 verifySelection(); 147 148 resizeSelection(new Point(214, 214)); 149 verifySelection(); 150 151 resizeSelection(new Point(215, 214)); 152 verifySelection(); 153 154 resizeSelection(new Point(214, 214)); 155 verifySelection(); 156 157 resizeSelection(new Point(110, 110)); 158 verifySelection(); 159 160 resizeSelection(new Point(110, 109)); 161 verifySelection(); 162 163 resizeSelection(new Point(109, 109)); 164 verifySelection(); 165 166 resizeSelection(new Point(5, 5)); 167 verifySelection(); 168 169 resizeSelection(new Point(0, 0)); 170 verifySelection(); 171 172 assertEquals(NOT_SET, mModel.getPositionNearestOrigin()); 173 } 174 175 @Test 176 public void testSelectionMovingAroundOrigin() { 177 initData(16, 4); 178 179 startSelection(new Point(210, 210)); 180 resizeSelection(new Point(mViewWidth - 1, 0)); 181 verifySelection(); 182 183 resizeSelection(new Point(0, 0)); 184 verifySelection(); 185 186 resizeSelection(new Point(0, 420)); 187 verifySelection(); 188 189 resizeSelection(new Point(mViewWidth - 1, 420)); 190 verifySelection(); 191 192 // This is manually figured and will need to be adjusted if the separator position is 193 // changed. 194 assertEquals(7, mModel.getPositionNearestOrigin()); 195 } 196 197 @Test 198 public void testScrollingBandSelect() { 199 initData(40, 4); 200 201 startSelection(new Point(0, 0)); 202 resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1)); 203 verifySelection(); 204 205 scroll(CHILD_VIEW_EDGE_PX); 206 verifySelection(); 207 208 resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1)); 209 verifySelection(); 210 211 scroll(CHILD_VIEW_EDGE_PX); 212 verifySelection(); 213 214 scroll(-2 * CHILD_VIEW_EDGE_PX); 215 verifySelection(); 216 217 resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1)); 218 verifySelection(); 219 220 assertEquals(0, mModel.getPositionNearestOrigin()); 221 } 222 223 private void initData(final int numChildren, int numColumns) { 224 mHost = new TestHost(numChildren, numColumns); 225 mAdapter = new TestAdapter() { 226 @Override 227 public String getStableId(int position) { 228 return Integer.toString(position); 229 } 230 231 @Override 232 public int getItemCount() { 233 return numChildren; 234 } 235 }; 236 237 mViewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX); 238 239 mModel = new GridModel( 240 mHost, 241 new TestStableIdProvider(mAdapter), 242 SelectionPredicates.CAN_SET_ANYTHING); 243 244 mModel.addOnSelectionChangedListener( 245 new GridModel.SelectionObserver() { 246 @Override 247 public void onSelectionChanged(Set<String> updatedSelection) { 248 mLastSelection = updatedSelection; 249 } 250 }); 251 } 252 253 /** Returns the current selection area as a Rect. */ 254 private Rect getSelectionArea() { 255 // Construct a rect from the two selection points. 256 Rect selectionArea = new Rect( 257 mSelectionOrigin.x, mSelectionOrigin.y, mSelectionOrigin.x, mSelectionOrigin.y); 258 selectionArea.union(mSelectionPoint.x, mSelectionPoint.y); 259 // Rect intersection tests are exclusive of bounds, while the MSM's selection code is 260 // inclusive. Expand the rect by 1 pixel in all directions to account for this. 261 selectionArea.inset(-1, -1); 262 263 return selectionArea; 264 } 265 266 /** Asserts that the selection is currently empty. */ 267 private void assertNoSelection() { 268 assertEquals("Unexpected items " + mLastSelection + " in selection " + getSelectionArea(), 269 0, mLastSelection.size()); 270 } 271 272 /** Verifies the selection using actual bbox checks. */ 273 private void verifySelection() { 274 Rect selectionArea = getSelectionArea(); 275 for (TestHost.Item item: mHost.items) { 276 if (Rect.intersects(selectionArea, item.rect)) { 277 assertTrue("Expected item " + item + " was not in selection " + selectionArea, 278 mLastSelection.contains(item.name)); 279 } else { 280 assertFalse("Unexpected item " + item + " in selection" + selectionArea, 281 mLastSelection.contains(item.name)); 282 } 283 } 284 } 285 286 private void startSelection(Point p) { 287 mModel.startCapturing(p); 288 mSelectionOrigin = mHost.createAbsolutePoint(p); 289 } 290 291 private void resizeSelection(Point p) { 292 mModel.resizeSelection(p); 293 mSelectionPoint = mHost.createAbsolutePoint(p); 294 } 295 296 private void scroll(int dy) { 297 assertTrue(mHost.verticalOffset + VIEWPORT_HEIGHT + dy <= mHost.getTotalHeight()); 298 mHost.verticalOffset += dy; 299 // Correct the cached selection point as well. 300 mSelectionPoint.y += dy; 301 mHost.mScrollListener.onScrolled(null, 0, dy); 302 } 303 304 private static final class TestHost extends BandHost { 305 306 private final int mNumColumns; 307 private final int mNumRows; 308 private final int mNumChildren; 309 private final int mSeparatorPosition; 310 311 public int horizontalOffset = 0; 312 public int verticalOffset = 0; 313 private List<Item> items = new ArrayList<>(); 314 315 // Installed by GridModel on construction. 316 private @Nullable OnScrollListener mScrollListener; 317 318 public TestHost(int numChildren, int numColumns) { 319 mNumChildren = numChildren; 320 mNumColumns = numColumns; 321 mSeparatorPosition = mNumColumns + 1; 322 mNumRows = setupGrid(); 323 } 324 325 private int setupGrid() { 326 // Split the input set into folders and documents. Do this such that there is a 327 // partially-populated row in the middle of the grid, to test corner cases in layout 328 // code. 329 int y = VIEW_PADDING_PX; 330 int i = 0; 331 int numRows = 0; 332 while (i < mNumChildren) { 333 int top = y; 334 int height = CHILD_VIEW_EDGE_PX; 335 int width = CHILD_VIEW_EDGE_PX; 336 for (int j = 0; j < mNumColumns && i < mNumChildren; j++) { 337 int left = VIEW_PADDING_PX + (j * (width + VIEW_PADDING_PX)); 338 items.add(new Item( 339 Integer.toString(i), 340 new Rect( 341 left, 342 top, 343 left + width - 1, 344 top + height - 1))); 345 346 // Create a partially populated row at the separator position. 347 if (++i == mSeparatorPosition) { 348 break; 349 } 350 } 351 y += height + VIEW_PADDING_PX; 352 numRows++; 353 } 354 355 return numRows; 356 } 357 358 private int getTotalHeight() { 359 return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1); 360 } 361 362 private int getFirstVisibleRowIndex() { 363 return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX); 364 } 365 366 private int getLastVisibleRowIndex() { 367 int lastVisibleRowUncapped = 368 (VIEWPORT_HEIGHT + verticalOffset - 1) / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX); 369 return Math.min(lastVisibleRowUncapped, mNumRows - 1); 370 } 371 372 private int getNumItemsInRow(int index) { 373 assertTrue(index >= 0 && index < mNumRows); 374 int mod = mSeparatorPosition % mNumColumns; 375 if (index == (mSeparatorPosition / mNumColumns)) { 376 // The row containing the separator may be incomplete 377 return mod > 0 ? mod : mNumColumns; 378 } 379 // Account for the partial separator row in the final row tally. 380 if (index == mNumRows - 1) { 381 // The last row may be incomplete 382 int finalRowCount = (mNumChildren - mod) % mNumColumns; 383 return finalRowCount > 0 ? finalRowCount : mNumColumns; 384 } 385 386 return mNumColumns; 387 } 388 389 @Override 390 public void addOnScrollListener(OnScrollListener listener) { 391 mScrollListener = listener; 392 } 393 394 @Override 395 public void removeOnScrollListener(OnScrollListener listener) {} 396 397 @Override 398 public Point createAbsolutePoint(Point relativePoint) { 399 return new Point( 400 relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset); 401 } 402 403 @Override 404 public int getVisibleChildCount() { 405 int childCount = 0; 406 for (int i = getFirstVisibleRowIndex(); i <= getLastVisibleRowIndex(); i++) { 407 childCount += getNumItemsInRow(i); 408 } 409 return childCount; 410 } 411 412 @Override 413 public int getAdapterPositionAt(int index) { 414 // Account for partial rows by actually tallying up the items in hidden rows. 415 int hiddenCount = 0; 416 for (int i = 0; i < getFirstVisibleRowIndex(); i++) { 417 hiddenCount += getNumItemsInRow(i); 418 } 419 return index + hiddenCount; 420 } 421 422 @Override 423 public Rect getAbsoluteRectForChildViewAt(int index) { 424 int adapterPosition = getAdapterPositionAt(index); 425 return items.get(adapterPosition).rect; 426 } 427 428 @Override 429 public int getChildCount() { 430 return mNumChildren; 431 } 432 433 @Override 434 public int getColumnCount() { 435 return mNumColumns; 436 } 437 438 @Override 439 public void showBand(Rect rect) { 440 throw new UnsupportedOperationException(); 441 } 442 443 @Override 444 public void hideBand() { 445 throw new UnsupportedOperationException(); 446 } 447 448 @Override 449 public void scrollBy(int dy) { 450 throw new UnsupportedOperationException(); 451 } 452 453 @Override 454 public int getHeight() { 455 throw new UnsupportedOperationException(); 456 } 457 458 @Override 459 public void invalidateView() { 460 throw new UnsupportedOperationException(); 461 } 462 463 @Override 464 public void runAtNextFrame(Runnable r) { 465 throw new UnsupportedOperationException(); 466 } 467 468 @Override 469 public void removeCallback(Runnable r) { 470 throw new UnsupportedOperationException(); 471 } 472 473 @Override 474 public boolean hasView(int adapterPosition) { 475 return true; 476 } 477 478 public static final class Item { 479 public String name; 480 public Rect rect; 481 482 public Item(String n, Rect r) { 483 name = n; 484 rect = r; 485 } 486 487 @Override 488 public String toString() { 489 return name + ": " + rect; 490 } 491 } 492 } 493 } 494