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.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