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