Home | History | Annotate | Download | only in selection
      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