• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
tearDown()71     public void tearDown() {
72         mModel = null;
73         mHost = null;
74         mLastSelection = null;
75     }
76 
77     @Test
testSelectionLeftOfItems()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
testSelectionRightOfItems()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
testSelectionAboveItems()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
testSelectionBelowItems()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
testVerticalSelectionBetweenItems()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
testHorizontalSelectionBetweenItems()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
testGrowingAndShrinkingSelection()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
testSelectionMovingAroundOrigin()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
testScrollingBandSelect()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 
initData(final int numChildren, int numColumns)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. */
getSelectionArea()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. */
assertNoSelection()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. */
verifySelection()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 
startSelection(Point p)286     private void startSelection(Point p) {
287         mModel.startCapturing(p);
288         mSelectionOrigin = mHost.createAbsolutePoint(p);
289     }
290 
resizeSelection(Point p)291     private void resizeSelection(Point p) {
292         mModel.resizeSelection(p);
293         mSelectionPoint = mHost.createAbsolutePoint(p);
294     }
295 
scroll(int dy)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 
TestHost(int numChildren, int numColumns)318         public TestHost(int numChildren, int numColumns) {
319             mNumChildren = numChildren;
320             mNumColumns = numColumns;
321             mSeparatorPosition = mNumColumns + 1;
322             mNumRows = setupGrid();
323         }
324 
setupGrid()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 
getTotalHeight()358         private int getTotalHeight() {
359             return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1);
360         }
361 
getFirstVisibleRowIndex()362         private int getFirstVisibleRowIndex() {
363             return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
364         }
365 
getLastVisibleRowIndex()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 
getNumItemsInRow(int index)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
addOnScrollListener(OnScrollListener listener)390         public void addOnScrollListener(OnScrollListener listener) {
391             mScrollListener = listener;
392         }
393 
394         @Override
removeOnScrollListener(OnScrollListener listener)395         public void removeOnScrollListener(OnScrollListener listener) {}
396 
397         @Override
createAbsolutePoint(Point relativePoint)398         public Point createAbsolutePoint(Point relativePoint) {
399             return new Point(
400                     relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset);
401         }
402 
403         @Override
getVisibleChildCount()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
getAdapterPositionAt(int index)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
getAbsoluteRectForChildViewAt(int index)423         public Rect getAbsoluteRectForChildViewAt(int index) {
424             int adapterPosition = getAdapterPositionAt(index);
425             return items.get(adapterPosition).rect;
426         }
427 
428         @Override
getChildCount()429         public int getChildCount() {
430             return mNumChildren;
431         }
432 
433         @Override
getColumnCount()434         public int getColumnCount() {
435             return mNumColumns;
436         }
437 
438         @Override
showBand(Rect rect)439         public void showBand(Rect rect) {
440             throw new UnsupportedOperationException();
441         }
442 
443         @Override
hideBand()444         public void hideBand() {
445             throw new UnsupportedOperationException();
446         }
447 
448         @Override
scrollBy(int dy)449         public void scrollBy(int dy) {
450             throw new UnsupportedOperationException();
451         }
452 
453         @Override
getHeight()454         public int getHeight() {
455             throw new UnsupportedOperationException();
456         }
457 
458         @Override
invalidateView()459         public void invalidateView() {
460             throw new UnsupportedOperationException();
461         }
462 
463         @Override
runAtNextFrame(Runnable r)464         public void runAtNextFrame(Runnable r) {
465             throw new UnsupportedOperationException();
466         }
467 
468         @Override
removeCallback(Runnable r)469         public void removeCallback(Runnable r) {
470             throw new UnsupportedOperationException();
471         }
472 
473         @Override
hasView(int adapterPosition)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 
Item(String n, Rect r)482             public Item(String n, Rect r) {
483                 name = n;
484                 rect = r;
485             }
486 
487             @Override
toString()488             public String toString() {
489                 return name + ": " + rect;
490             }
491         }
492     }
493 }
494