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