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 
initData(final int numChildren, int numColumns)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
tearDown()89     public void tearDown() {
90         model = null;
91         env = null;
92         lastSelection = null;
93     }
94 
testSelectionLeftOfItems()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 
testSelectionRightOfItems()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 
testSelectionAboveItems()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 
testSelectionBelowItems()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 
testVerticalSelectionBetweenItems()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 
testHorizontalSelectionBetweenItems()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 
testGrowingAndShrinkingSelection()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 
testSelectionMovingAroundOrigin()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 
testScrollingBandSelect()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. */
getSelectionArea()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. */
assertNoSelection()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. */
verifySelection()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 
startSelection(Point p)265     private void startSelection(Point p) {
266         model.startSelection(p);
267         mSelectionOrigin = env.createAbsolutePoint(p);
268     }
269 
resizeSelection(Point p)270     private void resizeSelection(Point p) {
271         model.resizeSelection(p);
272         mSelectionPoint = env.createAbsolutePoint(p);
273     }
274 
scroll(int dy)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 
TestEnvironment(int numChildren, int numColumns)294         public TestEnvironment(int numChildren, int numColumns) {
295             mNumChildren = numChildren;
296             mNumColumns = numColumns;
297             mSeparatorPosition = mNumColumns + 1;
298             mNumRows = setupGrid();
299         }
300 
setupGrid()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 
getTotalHeight()334         private int getTotalHeight() {
335             return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1);
336         }
337 
getFirstVisibleRowIndex()338         private int getFirstVisibleRowIndex() {
339             return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
340         }
341 
getLastVisibleRowIndex()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 
getNumItemsInRow(int index)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
addOnScrollListener(OnScrollListener listener)366         public void addOnScrollListener(OnScrollListener listener) {}
367 
368         @Override
removeOnScrollListener(OnScrollListener listener)369         public void removeOnScrollListener(OnScrollListener listener) {}
370 
371         @Override
createAbsolutePoint(Point relativePoint)372         public Point createAbsolutePoint(Point relativePoint) {
373             return new Point(
374                     relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset);
375         }
376 
377         @Override
getVisibleChildCount()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
getAdapterPositionAt(int index)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
getAbsoluteRectForChildViewAt(int index)397         public Rect getAbsoluteRectForChildViewAt(int index) {
398             int adapterPosition = getAdapterPositionAt(index);
399             return items.get(adapterPosition).rect;
400         }
401 
402         @Override
getChildCount()403         public int getChildCount() {
404             return mNumChildren;
405         }
406 
407         @Override
getColumnCount()408         public int getColumnCount() {
409             return mNumColumns;
410         }
411 
412         @Override
showBand(Rect rect)413         public void showBand(Rect rect) {
414             throw new UnsupportedOperationException();
415         }
416 
417         @Override
hideBand()418         public void hideBand() {
419             throw new UnsupportedOperationException();
420         }
421 
422         @Override
scrollBy(int dy)423         public void scrollBy(int dy) {
424             throw new UnsupportedOperationException();
425         }
426 
427         @Override
getHeight()428         public int getHeight() {
429             throw new UnsupportedOperationException();
430         }
431 
432         @Override
invalidateView()433         public void invalidateView() {
434             throw new UnsupportedOperationException();
435         }
436 
437         @Override
runAtNextFrame(Runnable r)438         public void runAtNextFrame(Runnable r) {
439             throw new UnsupportedOperationException();
440         }
441 
442         @Override
removeCallback(Runnable r)443         public void removeCallback(Runnable r) {
444             throw new UnsupportedOperationException();
445         }
446 
447         @Override
hasView(int adapterPosition)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 
Item(String n, Rect r)456             public Item(String n, Rect r) {
457                 name = n;
458                 rect = r;
459             }
460 
toString()461             public String toString() {
462                 return name + ": " + rect;
463             }
464         }
465     }
466 }
467