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