/* * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.recyclerview.selection; import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.os.Bundle; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.util.SparseBooleanArray; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.testing.Bundles; import androidx.recyclerview.selection.testing.SelectionProbe; import androidx.recyclerview.selection.testing.TestAdapter; import androidx.recyclerview.selection.testing.TestItemKeyProvider; import androidx.recyclerview.selection.testing.TestSelectionObserver; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @RunWith(AndroidJUnit4.class) @SmallTest public class DefaultSelectionTrackerTest { private static final String SELECTION_ID = "test-selection"; private List mItems; private Set mIgnored; private TestAdapter mAdapter; private DefaultSelectionTracker mTracker; private TestSelectionObserver mListener; private SelectionProbe mSelection; @Before public void setUp() throws Exception { mIgnored = new HashSet<>(); mItems = TestAdapter.createItemList(100); mListener = new TestSelectionObserver<>(); mAdapter = new TestAdapter(); mAdapter.updateTestModelIds(mItems); SelectionPredicate selectionPredicate = new SelectionPredicate() { @Override public boolean canSetStateForKey(String id, boolean nextState) { return !nextState || !mIgnored.contains(id); } @Override public boolean canSetStateAtPosition(int position, boolean nextState) { throw new UnsupportedOperationException("Not implemented."); } @Override public boolean canSelectMultiple() { return true; } }; ItemKeyProvider keyProvider = new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, mAdapter); mTracker = new DefaultSelectionTracker<>( SELECTION_ID, keyProvider, selectionPredicate, StorageStrategy.createStringStorage()); EventBridge.install(mAdapter, mTracker, keyProvider); mTracker.addObserver(mListener); mSelection = new SelectionProbe(mTracker, mListener); mIgnored.clear(); } @Test public void testSelect() { mTracker.select(mItems.get(7)); mSelection.assertSelection(7); } @Test public void testDeselect() { mTracker.select(mItems.get(7)); mTracker.deselect(mItems.get(7)); mSelection.assertNoSelection(); } @Test public void testSelection_DoNothingOnUnselectableItem() { mIgnored.add(mItems.get(7)); boolean selected = mTracker.select(mItems.get(7)); assertFalse(selected); mSelection.assertNoSelection(); } @Test public void testSelect_NotifiesListenersOfChange() { mTracker.select(mItems.get(7)); mListener.assertSelectionChanged(); } @Test public void testSelect_NotifiesAdapterOfSelect() { mTracker.select(mItems.get(7)); mAdapter.assertNotifiedOfSelectionChange(7); } @Test public void testSelect_NotifiesAdapterOfDeselect() { mTracker.select(mItems.get(7)); mAdapter.resetSelectionNotifications(); mTracker.deselect(mItems.get(7)); mAdapter.assertNotifiedOfSelectionChange(7); } @Test public void testDeselect_NotifiesSelectionChanged() { mTracker.select(mItems.get(7)); mTracker.deselect(mItems.get(7)); mListener.assertSelectionChanged(); } @Test public void testSelection_PersistsOnUpdate() { mTracker.select(mItems.get(7)); mAdapter.updateTestModelIds(mItems); mSelection.assertSelection(7); } @Test public void testSetItemsSelected() { mTracker.setItemsSelected(getStringIds(6, 7, 8), true); mSelection.assertRangeSelected(6, 8); } @Test public void testSetItemsSelected_SkipUnselectableItem() { mIgnored.add(mItems.get(7)); mTracker.setItemsSelected(getStringIds(6, 7, 8), true); mSelection.assertSelected(6); mSelection.assertNotSelected(7); mSelection.assertSelected(8); } @Test public void testClearSelection_RemovesPrimarySelection() { mTracker.select(mItems.get(1)); mTracker.select(mItems.get(2)); assertTrue(mTracker.clearSelection()); assertFalse(mTracker.hasSelection()); } @Test public void testClearSelection_RemovesProvisionalSelection() { Set prov = new HashSet<>(); prov.add(mItems.get(1)); prov.add(mItems.get(2)); assertFalse(mTracker.clearSelection()); assertFalse(mTracker.hasSelection()); } @Test public void testRangeSelection() { mTracker.startRange(15); mTracker.extendRange(19); mSelection.assertRangeSelection(15, 19); } @Test public void testRangeSelection_SkipUnselectableItem() { mIgnored.add(mItems.get(17)); mTracker.startRange(15); mTracker.extendRange(19); mSelection.assertRangeSelected(15, 16); mSelection.assertNotSelected(17); mSelection.assertRangeSelected(18, 19); } @Test public void testRangeSelection_snapExpand() { mTracker.startRange(15); mTracker.extendRange(19); mTracker.extendRange(27); mSelection.assertRangeSelection(15, 27); } @Test public void testRangeSelection_snapContract() { mTracker.startRange(15); mTracker.extendRange(27); mTracker.extendRange(19); mSelection.assertRangeSelection(15, 19); } @Test public void testRangeSelection_snapInvert() { mTracker.startRange(15); mTracker.extendRange(27); mTracker.extendRange(3); mSelection.assertRangeSelection(3, 15); } @Test public void testRangeSelection_multiple() { mTracker.startRange(15); mTracker.extendRange(27); mTracker.endRange(); mTracker.startRange(42); mTracker.extendRange(57); mSelection.assertSelectionSize(29); mSelection.assertRangeSelected(15, 27); mSelection.assertRangeSelected(42, 57); } @Test public void testProvisionalRangeSelection() { mTracker.startRange(13); mTracker.extendProvisionalRange(15); mSelection.assertRangeSelection(13, 15); mTracker.getSelection().mergeProvisionalSelection(); mTracker.endRange(); mSelection.assertSelectionSize(3); } @Test public void testProvisionalRangeSelection_endEarly() { mTracker.startRange(13); mTracker.extendProvisionalRange(15); mSelection.assertRangeSelection(13, 15); mTracker.endRange(); // If we end range selection prematurely for provision selection, nothing should be selected // except the first item mSelection.assertSelectionSize(1); } @Test public void testProvisionalRangeSelection_snapExpand() { mTracker.startRange(13); mTracker.extendProvisionalRange(15); mSelection.assertRangeSelection(13, 15); mTracker.getSelection().mergeProvisionalSelection(); mTracker.extendRange(18); mSelection.assertRangeSelection(13, 18); } @Test public void testCombinationRangeSelection_IntersectsOldSelection() { mTracker.startRange(13); mTracker.extendRange(15); mSelection.assertRangeSelection(13, 15); mTracker.startRange(11); mTracker.extendProvisionalRange(18); mSelection.assertRangeSelected(11, 18); mTracker.endRange(); mSelection.assertRangeSelected(13, 15); mSelection.assertRangeSelected(11, 11); mSelection.assertSelectionSize(4); } @Test public void testProvisionalSelection() { Selection s = mTracker.getSelection(); mSelection.assertNoSelection(); // Mimicking band selection case -- BandController notifies item callback by itself. mListener.onItemStateChanged(mItems.get(1), true); mListener.onItemStateChanged(mItems.get(2), true); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); mSelection.assertSelection(1, 2); } @Test public void testProvisionalSelection_Replace() { Selection s = mTracker.getSelection(); // Mimicking band selection case -- BandController notifies item callback by itself. mListener.onItemStateChanged(mItems.get(1), true); mListener.onItemStateChanged(mItems.get(2), true); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); mListener.onItemStateChanged(mItems.get(1), false); mListener.onItemStateChanged(mItems.get(2), false); provisional.clear(); mListener.onItemStateChanged(mItems.get(3), true); mListener.onItemStateChanged(mItems.get(4), true); provisional.append(3, true); provisional.append(4, true); s.setProvisionalSelection(getItemIds(provisional)); mSelection.assertSelection(3, 4); } @Test public void testProvisionalSelection_IntersectsExistingProvisionalSelection() { Selection s = mTracker.getSelection(); // Mimicking band selection case -- BandController notifies item callback by itself. mListener.onItemStateChanged(mItems.get(1), true); mListener.onItemStateChanged(mItems.get(2), true); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); mListener.onItemStateChanged(mItems.get(1), false); mListener.onItemStateChanged(mItems.get(2), false); provisional.clear(); mListener.onItemStateChanged(mItems.get(1), true); provisional.append(1, true); s.setProvisionalSelection(getItemIds(provisional)); mSelection.assertSelection(1); } @Test public void testProvisionalSelection_Apply() { Selection s = mTracker.getSelection(); // Mimicking band selection case -- BandController notifies item callback by itself. mListener.onItemStateChanged(mItems.get(1), true); mListener.onItemStateChanged(mItems.get(2), true); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); s.mergeProvisionalSelection(); mSelection.assertSelection(1, 2); } @Test public void testProvisionalSelection_Cancel() { mTracker.select(mItems.get(1)); mTracker.select(mItems.get(2)); Selection s = mTracker.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(3, true); provisional.append(4, true); s.setProvisionalSelection(getItemIds(provisional)); s.clearProvisionalSelection(); // Original selection should remain. mSelection.assertSelection(1, 2); } @Test public void testProvisionalSelection_IntersectsAppliedSelection() { mTracker.select(mItems.get(1)); mTracker.select(mItems.get(2)); Selection s = mTracker.getSelection(); // Mimicking band selection case -- BandController notifies item callback by itself. mListener.onItemStateChanged(mItems.get(3), true); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(2, true); provisional.append(3, true); s.setProvisionalSelection(getItemIds(provisional)); mSelection.assertSelection(1, 2, 3); } private Set getItemIds(SparseBooleanArray selection) { Set ids = new HashSet<>(); int count = selection.size(); for (int i = 0; i < count; ++i) { ids.add(mItems.get(selection.keyAt(i))); } return ids; } @Test public void testObserverOnChanged_NotifiesListenersOfChange() { mAdapter.notifyDataSetChanged(); mListener.assertSelectionChanged(); } @Test public void testInstanceState() { Bundle state = new Bundle(); MutableSelection orig = new MutableSelection<>(); mTracker.select("10"); mTracker.select("20"); mTracker.copySelection(orig); mTracker.onSaveInstanceState(state); mTracker.clearSelection(); Bundle parceled = Bundles.forceParceling(state); mTracker.onRestoreInstanceState(parceled); assertEquals(orig, mTracker.getSelection()); } @Test public void testIgnoresNullBundle() { mTracker.onRestoreInstanceState(null); // simply doesn't blow up. } private Iterable getStringIds(int... ids) { List stringIds = new ArrayList<>(ids.length); for (int id : ids) { stringIds.add(mItems.get(id)); } return stringIds; } }