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