Home | History | Annotate | Download | only in selection
      1 /*
      2  * Copyright 2017 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 androidx.recyclerview.selection;
     18 
     19 import static junit.framework.Assert.assertEquals;
     20 
     21 import static org.junit.Assert.assertFalse;
     22 import static org.junit.Assert.assertTrue;
     23 
     24 import android.os.Bundle;
     25 import android.support.test.filters.SmallTest;
     26 import android.support.test.runner.AndroidJUnit4;
     27 import android.util.SparseBooleanArray;
     28 
     29 import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
     30 import androidx.recyclerview.selection.testing.Bundles;
     31 import androidx.recyclerview.selection.testing.SelectionProbe;
     32 import androidx.recyclerview.selection.testing.TestAdapter;
     33 import androidx.recyclerview.selection.testing.TestItemKeyProvider;
     34 import androidx.recyclerview.selection.testing.TestSelectionObserver;
     35 
     36 import org.junit.Before;
     37 import org.junit.Test;
     38 import org.junit.runner.RunWith;
     39 
     40 import java.util.ArrayList;
     41 import java.util.HashSet;
     42 import java.util.List;
     43 import java.util.Set;
     44 
     45 @RunWith(AndroidJUnit4.class)
     46 @SmallTest
     47 public class DefaultSelectionTrackerTest {
     48 
     49     private static final String SELECTION_ID = "test-selection";
     50 
     51     private List<String> mItems;
     52     private Set<String> mIgnored;
     53     private TestAdapter mAdapter;
     54     private DefaultSelectionTracker<String> mTracker;
     55     private TestSelectionObserver<String> mListener;
     56     private SelectionProbe mSelection;
     57 
     58     @Before
     59     public void setUp() throws Exception {
     60         mIgnored = new HashSet<>();
     61         mItems = TestAdapter.createItemList(100);
     62         mListener = new TestSelectionObserver<>();
     63         mAdapter = new TestAdapter();
     64         mAdapter.updateTestModelIds(mItems);
     65 
     66         SelectionPredicate selectionPredicate = new SelectionPredicate<String>() {
     67 
     68             @Override
     69             public boolean canSetStateForKey(String id, boolean nextState) {
     70                 return !nextState || !mIgnored.contains(id);
     71             }
     72 
     73             @Override
     74             public boolean canSetStateAtPosition(int position, boolean nextState) {
     75                 throw new UnsupportedOperationException("Not implemented.");
     76             }
     77 
     78             @Override
     79             public boolean canSelectMultiple() {
     80                 return true;
     81             }
     82         };
     83 
     84         ItemKeyProvider<String> keyProvider =
     85                 new TestItemKeyProvider<String>(ItemKeyProvider.SCOPE_MAPPED, mAdapter);
     86 
     87         mTracker = new DefaultSelectionTracker<>(
     88                 SELECTION_ID,
     89                 keyProvider,
     90                 selectionPredicate,
     91                 StorageStrategy.createStringStorage());
     92 
     93         EventBridge.install(mAdapter, mTracker, keyProvider);
     94 
     95         mTracker.addObserver(mListener);
     96 
     97         mSelection = new SelectionProbe(mTracker, mListener);
     98 
     99         mIgnored.clear();
    100     }
    101 
    102     @Test
    103     public void testSelect() {
    104         mTracker.select(mItems.get(7));
    105 
    106         mSelection.assertSelection(7);
    107     }
    108 
    109     @Test
    110     public void testDeselect() {
    111         mTracker.select(mItems.get(7));
    112         mTracker.deselect(mItems.get(7));
    113 
    114         mSelection.assertNoSelection();
    115     }
    116 
    117     @Test
    118     public void testSelection_DoNothingOnUnselectableItem() {
    119         mIgnored.add(mItems.get(7));
    120         boolean selected = mTracker.select(mItems.get(7));
    121 
    122         assertFalse(selected);
    123         mSelection.assertNoSelection();
    124     }
    125 
    126     @Test
    127     public void testSelect_NotifiesListenersOfChange() {
    128         mTracker.select(mItems.get(7));
    129 
    130         mListener.assertSelectionChanged();
    131     }
    132 
    133     @Test
    134     public void testSelect_NotifiesAdapterOfSelect() {
    135         mTracker.select(mItems.get(7));
    136 
    137         mAdapter.assertNotifiedOfSelectionChange(7);
    138     }
    139 
    140     @Test
    141     public void testSelect_NotifiesAdapterOfDeselect() {
    142         mTracker.select(mItems.get(7));
    143         mAdapter.resetSelectionNotifications();
    144         mTracker.deselect(mItems.get(7));
    145         mAdapter.assertNotifiedOfSelectionChange(7);
    146     }
    147 
    148     @Test
    149     public void testDeselect_NotifiesSelectionChanged() {
    150         mTracker.select(mItems.get(7));
    151         mTracker.deselect(mItems.get(7));
    152 
    153         mListener.assertSelectionChanged();
    154     }
    155 
    156     @Test
    157     public void testSelection_PersistsOnUpdate() {
    158         mTracker.select(mItems.get(7));
    159         mAdapter.updateTestModelIds(mItems);
    160 
    161         mSelection.assertSelection(7);
    162     }
    163 
    164     @Test
    165     public void testSetItemsSelected() {
    166         mTracker.setItemsSelected(getStringIds(6, 7, 8), true);
    167 
    168         mSelection.assertRangeSelected(6, 8);
    169     }
    170 
    171     @Test
    172     public void testSetItemsSelected_SkipUnselectableItem() {
    173         mIgnored.add(mItems.get(7));
    174 
    175         mTracker.setItemsSelected(getStringIds(6, 7, 8), true);
    176 
    177         mSelection.assertSelected(6);
    178         mSelection.assertNotSelected(7);
    179         mSelection.assertSelected(8);
    180     }
    181 
    182     @Test
    183     public void testClearSelection_RemovesPrimarySelection() {
    184         mTracker.select(mItems.get(1));
    185         mTracker.select(mItems.get(2));
    186 
    187         assertTrue(mTracker.clearSelection());
    188 
    189         assertFalse(mTracker.hasSelection());
    190     }
    191 
    192     @Test
    193     public void testClearSelection_RemovesProvisionalSelection() {
    194         Set<String> prov = new HashSet<>();
    195         prov.add(mItems.get(1));
    196         prov.add(mItems.get(2));
    197 
    198         assertFalse(mTracker.clearSelection());
    199         assertFalse(mTracker.hasSelection());
    200     }
    201 
    202     @Test
    203     public void testRangeSelection() {
    204         mTracker.startRange(15);
    205         mTracker.extendRange(19);
    206         mSelection.assertRangeSelection(15, 19);
    207     }
    208 
    209     @Test
    210     public void testRangeSelection_SkipUnselectableItem() {
    211         mIgnored.add(mItems.get(17));
    212 
    213         mTracker.startRange(15);
    214         mTracker.extendRange(19);
    215 
    216         mSelection.assertRangeSelected(15, 16);
    217         mSelection.assertNotSelected(17);
    218         mSelection.assertRangeSelected(18, 19);
    219     }
    220 
    221     @Test
    222     public void testRangeSelection_snapExpand() {
    223         mTracker.startRange(15);
    224         mTracker.extendRange(19);
    225         mTracker.extendRange(27);
    226         mSelection.assertRangeSelection(15, 27);
    227     }
    228 
    229     @Test
    230     public void testRangeSelection_snapContract() {
    231         mTracker.startRange(15);
    232         mTracker.extendRange(27);
    233         mTracker.extendRange(19);
    234         mSelection.assertRangeSelection(15, 19);
    235     }
    236 
    237     @Test
    238     public void testRangeSelection_snapInvert() {
    239         mTracker.startRange(15);
    240         mTracker.extendRange(27);
    241         mTracker.extendRange(3);
    242         mSelection.assertRangeSelection(3, 15);
    243     }
    244 
    245     @Test
    246     public void testRangeSelection_multiple() {
    247         mTracker.startRange(15);
    248         mTracker.extendRange(27);
    249         mTracker.endRange();
    250         mTracker.startRange(42);
    251         mTracker.extendRange(57);
    252         mSelection.assertSelectionSize(29);
    253         mSelection.assertRangeSelected(15, 27);
    254         mSelection.assertRangeSelected(42, 57);
    255     }
    256 
    257     @Test
    258     public void testProvisionalRangeSelection() {
    259         mTracker.startRange(13);
    260         mTracker.extendProvisionalRange(15);
    261         mSelection.assertRangeSelection(13, 15);
    262         mTracker.getSelection().mergeProvisionalSelection();
    263         mTracker.endRange();
    264         mSelection.assertSelectionSize(3);
    265     }
    266 
    267     @Test
    268     public void testProvisionalRangeSelection_endEarly() {
    269         mTracker.startRange(13);
    270         mTracker.extendProvisionalRange(15);
    271         mSelection.assertRangeSelection(13, 15);
    272 
    273         mTracker.endRange();
    274         // If we end range selection prematurely for provision selection, nothing should be selected
    275         // except the first item
    276         mSelection.assertSelectionSize(1);
    277     }
    278 
    279     @Test
    280     public void testProvisionalRangeSelection_snapExpand() {
    281         mTracker.startRange(13);
    282         mTracker.extendProvisionalRange(15);
    283         mSelection.assertRangeSelection(13, 15);
    284         mTracker.getSelection().mergeProvisionalSelection();
    285         mTracker.extendRange(18);
    286         mSelection.assertRangeSelection(13, 18);
    287     }
    288 
    289     @Test
    290     public void testCombinationRangeSelection_IntersectsOldSelection() {
    291         mTracker.startRange(13);
    292         mTracker.extendRange(15);
    293         mSelection.assertRangeSelection(13, 15);
    294 
    295         mTracker.startRange(11);
    296         mTracker.extendProvisionalRange(18);
    297         mSelection.assertRangeSelected(11, 18);
    298         mTracker.endRange();
    299         mSelection.assertRangeSelected(13, 15);
    300         mSelection.assertRangeSelected(11, 11);
    301         mSelection.assertSelectionSize(4);
    302     }
    303 
    304     @Test
    305     public void testProvisionalSelection() {
    306         Selection<String> s = mTracker.getSelection();
    307         mSelection.assertNoSelection();
    308 
    309         // Mimicking band selection case -- BandController notifies item callback by itself.
    310         mListener.onItemStateChanged(mItems.get(1), true);
    311         mListener.onItemStateChanged(mItems.get(2), true);
    312 
    313         SparseBooleanArray provisional = new SparseBooleanArray();
    314         provisional.append(1, true);
    315         provisional.append(2, true);
    316         s.setProvisionalSelection(getItemIds(provisional));
    317         mSelection.assertSelection(1, 2);
    318     }
    319 
    320     @Test
    321     public void testProvisionalSelection_Replace() {
    322         Selection<String> s = mTracker.getSelection();
    323 
    324         // Mimicking band selection case -- BandController notifies item callback by itself.
    325         mListener.onItemStateChanged(mItems.get(1), true);
    326         mListener.onItemStateChanged(mItems.get(2), true);
    327         SparseBooleanArray provisional = new SparseBooleanArray();
    328         provisional.append(1, true);
    329         provisional.append(2, true);
    330         s.setProvisionalSelection(getItemIds(provisional));
    331 
    332         mListener.onItemStateChanged(mItems.get(1), false);
    333         mListener.onItemStateChanged(mItems.get(2), false);
    334         provisional.clear();
    335 
    336         mListener.onItemStateChanged(mItems.get(3), true);
    337         mListener.onItemStateChanged(mItems.get(4), true);
    338         provisional.append(3, true);
    339         provisional.append(4, true);
    340         s.setProvisionalSelection(getItemIds(provisional));
    341         mSelection.assertSelection(3, 4);
    342     }
    343 
    344     @Test
    345     public void testProvisionalSelection_IntersectsExistingProvisionalSelection() {
    346         Selection<String> s = mTracker.getSelection();
    347 
    348         // Mimicking band selection case -- BandController notifies item callback by itself.
    349         mListener.onItemStateChanged(mItems.get(1), true);
    350         mListener.onItemStateChanged(mItems.get(2), true);
    351         SparseBooleanArray provisional = new SparseBooleanArray();
    352         provisional.append(1, true);
    353         provisional.append(2, true);
    354         s.setProvisionalSelection(getItemIds(provisional));
    355 
    356         mListener.onItemStateChanged(mItems.get(1), false);
    357         mListener.onItemStateChanged(mItems.get(2), false);
    358         provisional.clear();
    359 
    360         mListener.onItemStateChanged(mItems.get(1), true);
    361         provisional.append(1, true);
    362         s.setProvisionalSelection(getItemIds(provisional));
    363         mSelection.assertSelection(1);
    364     }
    365 
    366     @Test
    367     public void testProvisionalSelection_Apply() {
    368         Selection<String> s = mTracker.getSelection();
    369 
    370         // Mimicking band selection case -- BandController notifies item callback by itself.
    371         mListener.onItemStateChanged(mItems.get(1), true);
    372         mListener.onItemStateChanged(mItems.get(2), true);
    373         SparseBooleanArray provisional = new SparseBooleanArray();
    374         provisional.append(1, true);
    375         provisional.append(2, true);
    376         s.setProvisionalSelection(getItemIds(provisional));
    377         s.mergeProvisionalSelection();
    378 
    379         mSelection.assertSelection(1, 2);
    380     }
    381 
    382     @Test
    383     public void testProvisionalSelection_Cancel() {
    384         mTracker.select(mItems.get(1));
    385         mTracker.select(mItems.get(2));
    386         Selection<String> s = mTracker.getSelection();
    387 
    388         SparseBooleanArray provisional = new SparseBooleanArray();
    389         provisional.append(3, true);
    390         provisional.append(4, true);
    391         s.setProvisionalSelection(getItemIds(provisional));
    392         s.clearProvisionalSelection();
    393 
    394         // Original selection should remain.
    395         mSelection.assertSelection(1, 2);
    396     }
    397 
    398     @Test
    399     public void testProvisionalSelection_IntersectsAppliedSelection() {
    400         mTracker.select(mItems.get(1));
    401         mTracker.select(mItems.get(2));
    402         Selection<String> s = mTracker.getSelection();
    403 
    404         // Mimicking band selection case -- BandController notifies item callback by itself.
    405         mListener.onItemStateChanged(mItems.get(3), true);
    406         SparseBooleanArray provisional = new SparseBooleanArray();
    407         provisional.append(2, true);
    408         provisional.append(3, true);
    409         s.setProvisionalSelection(getItemIds(provisional));
    410         mSelection.assertSelection(1, 2, 3);
    411     }
    412 
    413     private Set<String> getItemIds(SparseBooleanArray selection) {
    414         Set<String> ids = new HashSet<>();
    415 
    416         int count = selection.size();
    417         for (int i = 0; i < count; ++i) {
    418             ids.add(mItems.get(selection.keyAt(i)));
    419         }
    420 
    421         return ids;
    422     }
    423 
    424     @Test
    425     public void testObserverOnChanged_NotifiesListenersOfChange() {
    426         mAdapter.notifyDataSetChanged();
    427 
    428         mListener.assertSelectionChanged();
    429     }
    430 
    431     @Test
    432     public void testInstanceState() {
    433         Bundle state = new Bundle();
    434         MutableSelection<String> orig = new MutableSelection<>();
    435 
    436         mTracker.select("10");
    437         mTracker.select("20");
    438         mTracker.copySelection(orig);
    439 
    440         mTracker.onSaveInstanceState(state);
    441         mTracker.clearSelection();
    442 
    443         Bundle parceled = Bundles.forceParceling(state);
    444 
    445         mTracker.onRestoreInstanceState(parceled);
    446         assertEquals(orig, mTracker.getSelection());
    447     }
    448 
    449     @Test
    450     public void testIgnoresNullBundle() {
    451         mTracker.onRestoreInstanceState(null);  // simply doesn't blow up.
    452     }
    453 
    454     private Iterable<String> getStringIds(int... ids) {
    455         List<String> stringIds = new ArrayList<>(ids.length);
    456         for (int id : ids) {
    457             stringIds.add(mItems.get(id));
    458         }
    459         return stringIds;
    460     }
    461 }
    462