1 /*
2 * Copyright 2024 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.photopicker.core.selection
18 
19 import androidx.test.ext.junit.runners.AndroidJUnit4
20 import androidx.test.filters.SmallTest
21 import com.android.photopicker.core.configuration.MULTI_SELECT_CONFIG
22 import com.android.photopicker.core.configuration.SINGLE_SELECT_CONFIG
23 import com.android.photopicker.core.configuration.provideTestConfigurationFlow
24 import com.android.photopicker.data.model.Grantable
25 import com.google.common.truth.Truth.assertWithMessage
26 import kotlinx.coroutines.ExperimentalCoroutinesApi
27 import kotlinx.coroutines.flow.first
28 import kotlinx.coroutines.flow.toList
29 import kotlinx.coroutines.launch
30 import kotlinx.coroutines.test.advanceTimeBy
31 import kotlinx.coroutines.test.runTest
32 import org.junit.Test
33 import org.junit.runner.RunWith
34 
35 @SmallTest
36 @RunWith(AndroidJUnit4::class)
37 @OptIn(ExperimentalCoroutinesApi::class)
38 class GrantsAwareSelectionTest {
39 
40     /** A sample data class used only for testing. */
41     private data class SelectionData(val id: Int, val isPreGrantedParam: Boolean = false) :
42         Grantable {
43         override val isPreGranted: Boolean = isPreGrantedParam
44     }
45 
46     private val INITIAL_SELECTION =
<lambda>null47         buildSet<SelectionData> {
48             for (i in 1..10) {
49                 add(SelectionData(id = i))
50             }
51         }
52 
53     /** Ensures the selection is initialized as empty when no items are provided. */
54     @Test
<lambda>null55     fun testSelectionIsEmptyByDefault() = runTest {
56         val selection: Selection<SelectionData> =
57             GrantsAwareSelectionImpl(
58                 scope = backgroundScope,
59                 configuration =
60                 provideTestConfigurationFlow(
61                     scope = backgroundScope,
62                     defaultConfiguration = SINGLE_SELECT_CONFIG
63                 )
64             )
65         val snapshot = selection.snapshot()
66 
67         assertWithMessage("Snapshot was expected to be empty.").that(snapshot).isEmpty()
68         assertWithMessage("Emitted flow was expected to be empty.")
69             .that(selection.flow.first())
70             .isEmpty()
71     }
72 
73     /** Ensures the selection is initialized with the provided items. */
74     @Test
testSelectionIsInitializednull75     fun testSelectionIsInitialized() = runTest {
76         val selection: Selection<SelectionData> =
77             GrantsAwareSelectionImpl(
78                 scope = backgroundScope,
79                 configuration =
80                 provideTestConfigurationFlow(
81                     scope = backgroundScope,
82                     defaultConfiguration = MULTI_SELECT_CONFIG
83                 ),
84                 initialSelection = INITIAL_SELECTION
85             )
86 
87         val snapshot = selection.snapshot()
88         val flow = selection.flow.first()
89 
90         assertWithMessage("Snapshot was expected to contain the initial selection")
91             .that(snapshot)
92             .isEqualTo(INITIAL_SELECTION)
93         assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(10)
94 
95         assertWithMessage("Emitted flow was expected to contain the initial selection")
96             .that(flow)
97             .isEqualTo(INITIAL_SELECTION)
98         assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(10)
99     }
100 
101     @Test
<lambda>null102     fun testSelectionReturnsSuccess() = runTest {
103         val selection: Selection<SelectionData> =
104             GrantsAwareSelectionImpl(
105                 scope = backgroundScope,
106                 configuration =
107                 provideTestConfigurationFlow(
108                     scope = backgroundScope,
109                     defaultConfiguration = MULTI_SELECT_CONFIG
110                 ),
111             )
112 
113 
114         assertWithMessage("Selection addition was expected to be successful: item 1")
115             .that(selection.add(SelectionData(1)))
116             .isEqualTo(SelectionModifiedResult.SUCCESS)
117         assertWithMessage("Selection addition was expected to be successful: item 2")
118             .that(selection.toggle(SelectionData(2)))
119             .isEqualTo(SelectionModifiedResult.SUCCESS)
120         assertWithMessage("Selection addition was expected to be successful: item 3")
121             .that(selection.toggleAll(setOf(SelectionData(3))))
122             .isEqualTo(SelectionModifiedResult.SUCCESS)
123     }
124 
125     @Test
<lambda>null126     fun testSelectionReturnsSelectionLimitExceededWhenFull() = runTest {
127         val selection: Selection<SelectionData> =
128             GrantsAwareSelectionImpl(
129                 scope = backgroundScope,
130                 configuration =
131                 provideTestConfigurationFlow(
132                     scope = backgroundScope,
133                     defaultConfiguration = SINGLE_SELECT_CONFIG
134                 ),
135                 initialSelection = setOf(SelectionData(1))
136             )
137 
138 
139         assertWithMessage("Snapshot was expected to contain the initial selection")
140             .that(selection.add(SelectionData(2)))
141             .isEqualTo(SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED)
142     }
143 
144     /** Ensures a single item can be added to the selection. */
145     @Test
<lambda>null146     fun testSelectionCanAddSingleItem() = runTest {
147         val selection: Selection<SelectionData> =
148             GrantsAwareSelectionImpl(
149                 scope = backgroundScope,
150                 configuration =
151                 provideTestConfigurationFlow(
152                     scope = backgroundScope,
153                     defaultConfiguration = MULTI_SELECT_CONFIG
154                 )
155             )
156         val emissions = mutableListOf<Set<SelectionData>>()
157         backgroundScope.launch { selection.flow.toList(emissions) }
158 
159         val testItem = SelectionData(id = 999)
160         selection.add(testItem)
161 
162         val snapshot = selection.snapshot()
163         assertWithMessage("Snapshot does not contain the added item")
164             .that(snapshot)
165             .contains(testItem)
166         assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(1)
167 
168         advanceTimeBy(100)
169 
170         val flow = emissions.last()
171         assertWithMessage("Emitted flow value does not contain the added item.")
172             .that(flow)
173             .contains(testItem)
174         assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(1)
175     }
176 
177     /** Ensures bulk additions. */
178     @Test
<lambda>null179     fun testSelectionCanAddMultipleItems() = runTest {
180         val selection: Selection<SelectionData> =
181             GrantsAwareSelectionImpl(
182                 scope = backgroundScope,
183                 configuration =
184                 provideTestConfigurationFlow(
185                     scope = backgroundScope,
186                     defaultConfiguration = MULTI_SELECT_CONFIG
187                 )
188             )
189         val emissions = mutableListOf<Set<SelectionData>>()
190         backgroundScope.launch { selection.flow.toList(emissions) }
191 
192         val values =
193             setOf(
194                 SelectionData(id = 1),
195                 SelectionData(id = 2),
196                 SelectionData(id = 3),
197                 SelectionData(id = 4),
198                 SelectionData(id = 5),
199                 SelectionData(id = 6),
200             )
201         selection.addAll(values)
202 
203         advanceTimeBy(100)
204 
205         val snapshot = selection.snapshot()
206         assertWithMessage("Snapshot does not contain the added items")
207             .that(snapshot)
208             .containsExactly(*values.toTypedArray())
209         assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(6)
210 
211         assertWithMessage("Emitted flow does not contain the added items")
212             .that(emissions.last())
213             .containsExactly(*values.toTypedArray())
214         assertWithMessage("Emitted flow has an unexpected size").that(emissions.last()).hasSize(6)
215     }
216 
217     /** Ensures a selection can be reset. */
218     @Test
<lambda>null219     fun testSelectionCanBeCleared() = runTest {
220         val selection: Selection<SelectionData> =
221             GrantsAwareSelectionImpl(
222                 scope = backgroundScope,
223                 configuration =
224                 provideTestConfigurationFlow(
225                     scope = backgroundScope,
226                     defaultConfiguration = MULTI_SELECT_CONFIG
227                 ),
228                 initialSelection = INITIAL_SELECTION
229             )
230         val emissions = mutableListOf<Set<SelectionData>>()
231         backgroundScope.launch { selection.flow.toList(emissions) }
232 
233         assertWithMessage("Initial snapshot state does not match expected size")
234             .that(selection.snapshot())
235             .hasSize(10)
236 
237         selection.clear()
238 
239         assertWithMessage("Resulting snapshot does not match expected size")
240             .that(selection.snapshot())
241             .isEmpty()
242 
243         advanceTimeBy(100)
244 
245         assertWithMessage("Initial flow state does not match expected size")
246             .that(emissions.first())
247             .hasSize(10)
248 
249         assertWithMessage("Resulting flow state does not match expected size")
250             .that(emissions.last())
251             .isEmpty()
252     }
253 
254     /** Ensures a single item can be removed. */
255     @Test
<lambda>null256     fun testSelectionCanRemoveSingleItem() = runTest {
257         val testItem = SelectionData(id = 999)
258         val anotherTestItem = SelectionData(id = 1000)
259         val selection: Selection<SelectionData> =
260             GrantsAwareSelectionImpl(
261                 scope = backgroundScope,
262                 configuration =
263                 provideTestConfigurationFlow(
264                     scope = backgroundScope,
265                     defaultConfiguration = MULTI_SELECT_CONFIG
266                 ),
267                 initialSelection = setOf(testItem, anotherTestItem)
268             )
269         val emissions = mutableListOf<Set<SelectionData>>()
270         backgroundScope.launch { selection.flow.toList(emissions) }
271 
272         val initialSnapshot = selection.snapshot()
273         assertWithMessage("Initial Snapshot does not contain the expected item")
274             .that(initialSnapshot)
275             .isEqualTo(setOf(testItem, anotherTestItem))
276         assertWithMessage("Initial Snapshot has an unexpected size")
277             .that(initialSnapshot)
278             .hasSize(2)
279 
280         selection.remove(testItem)
281 
282         val snapshot = selection.snapshot()
283         assertWithMessage("Snapshot contains the removed item.")
284             .that(snapshot)
285             .doesNotContain(testItem)
286         assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(1)
287 
288         advanceTimeBy(100)
289 
290         val flow = emissions.last()
291         assertWithMessage("Emitted flow value contains the removed item.")
292             .that(flow)
293             .doesNotContain(testItem)
294         assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(1)
295     }
296 
297     /** Ensures bulk removals. */
298     @Test
<lambda>null299     fun testSelectionCanRemoveMultipleItems() = runTest {
300         val values =
301             setOf(
302                 SelectionData(id = 1),
303                 SelectionData(id = 2),
304                 SelectionData(id = 3),
305                 SelectionData(id = 4),
306                 SelectionData(id = 5),
307                 SelectionData(id = 6),
308             )
309 
310         val selection: Selection<SelectionData> =
311             GrantsAwareSelectionImpl(
312                 scope = backgroundScope,
313                 configuration =
314                 provideTestConfigurationFlow(
315                     scope = backgroundScope,
316                     defaultConfiguration = MULTI_SELECT_CONFIG
317                 ),
318                 initialSelection = values
319             )
320         val emissions = mutableListOf<Set<SelectionData>>()
321         backgroundScope.launch { selection.flow.toList(emissions) }
322 
323         val initialSnapshot = selection.snapshot()
324         assertWithMessage("Initial Snapshot has an unexpected size")
325             .that(initialSnapshot)
326             .hasSize(6)
327 
328         val removedValues = values.take(3)
329         selection.removeAll(removedValues)
330 
331         val snapshot = selection.snapshot()
332         assertWithMessage("Snapshot contains a removed item.")
333             .that(snapshot)
334             .containsNoneIn(removedValues.toTypedArray())
335         assertWithMessage("Snapshot has an unexpected size").that(snapshot).hasSize(3)
336 
337         advanceTimeBy(100)
338 
339         val flow = emissions.last()
340         assertWithMessage("Emitted flow value contains the removed item.")
341             .that(flow)
342             .containsNoneIn(removedValues.toTypedArray())
343         assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(3)
344     }
345 
346     /** Ensures a single item can be toggled in and out of the selected set. */
347     @Test
<lambda>null348     fun testSelectionCanToggleSingleItem() = runTest {
349         val selection: Selection<SelectionData> =
350             GrantsAwareSelectionImpl(
351                 scope = backgroundScope,
352                 configuration =
353                 provideTestConfigurationFlow(
354                     scope = backgroundScope,
355                     defaultConfiguration = MULTI_SELECT_CONFIG
356                 ),
357                 initialSelection = INITIAL_SELECTION
358             )
359         val emissions = mutableListOf<Set<SelectionData>>()
360         backgroundScope.launch { selection.flow.toList(emissions) }
361 
362         val item = INITIAL_SELECTION.first()
363 
364         selection.toggle(item)
365 
366         assertWithMessage("Snapshot contained an item that should have been removed")
367             .that(selection.snapshot())
368             .doesNotContain(item)
369 
370         advanceTimeBy(100)
371         assertWithMessage("Flow emission contained an item that should have been removed")
372             .that(emissions.last())
373             .doesNotContain(item)
374 
375         selection.toggle(item)
376 
377         assertWithMessage("Snapshot does not contain an item that should have been added")
378             .that(selection.snapshot())
379             .contains(item)
380 
381         advanceTimeBy(100)
382         assertWithMessage("Flow emission does not contain an item that should have been added")
383             .that(emissions.last())
384             .contains(item)
385     }
386 
387     /** Ensures multiple items can be toggled in and out of the selected set. */
388     @Test
<lambda>null389     fun testSelectionCanToggleMultipleItems() = runTest {
390         val selection: Selection<SelectionData> =
391             GrantsAwareSelectionImpl(
392                 scope = backgroundScope,
393                 configuration =
394                 provideTestConfigurationFlow(
395                     scope = backgroundScope,
396                     defaultConfiguration = MULTI_SELECT_CONFIG
397                 ),
398                 initialSelection = INITIAL_SELECTION
399             )
400         val emissions = mutableListOf<Set<SelectionData>>()
401         backgroundScope.launch { selection.flow.toList(emissions) }
402 
403         val items = INITIAL_SELECTION.take(3)
404 
405         selection.toggleAll(items)
406 
407         assertWithMessage("Snapshot contained an item that should have been removed")
408             .that(selection.snapshot())
409             .containsNoneIn(items)
410 
411         advanceTimeBy(100)
412         assertWithMessage("Flow emission contained an item that should have been removed")
413             .that(emissions.last())
414             .containsNoneIn(items)
415 
416         selection.toggleAll(items)
417 
418         assertWithMessage("Snapshot does not contain an item that should have been added")
419             .that(selection.snapshot())
420             .containsAtLeastElementsIn(items)
421 
422         advanceTimeBy(100)
423         assertWithMessage("Flow emission does not contain an item that should have been added")
424             .that(emissions.last())
425             .containsAtLeastElementsIn(items)
426     }
427 
428     /** Ensures selection returns the correct position for selected items. */
429     @Test
<lambda>null430     fun testSelectionCanReturnItemPosition() = runTest {
431         val values =
432             listOf(
433                 SelectionData(id = 1),
434                 SelectionData(id = 2),
435                 SelectionData(id = 3),
436                 SelectionData(id = 4),
437                 SelectionData(id = 5),
438                 SelectionData(id = 6),
439             )
440 
441         val selection: Selection<SelectionData> =
442             GrantsAwareSelectionImpl(
443                 scope = backgroundScope,
444                 configuration =
445                 provideTestConfigurationFlow(
446                     scope = backgroundScope,
447                     defaultConfiguration = MULTI_SELECT_CONFIG
448                 ),
449                 initialSelection = values
450             )
451 
452         assertWithMessage("Received unexpected position for item.")
453             .that(selection.getPosition(values.get(2)))
454             .isEqualTo(2)
455     }
456 
457     /** Ensures selection returns -1 for items not present in the selection. */
458     @Test
<lambda>null459     fun testSelectionGetPositionForMissingItem() = runTest {
460         val selection: Selection<SelectionData> =
461             GrantsAwareSelectionImpl(
462                 scope = backgroundScope,
463                 configuration =
464                 provideTestConfigurationFlow(
465                     scope = backgroundScope,
466                     defaultConfiguration = MULTI_SELECT_CONFIG
467                 ),
468                 initialSelection = INITIAL_SELECTION
469             )
470 
471         val missingElement = SelectionData(id = 999)
472 
473         assertWithMessage("Received unexpected position for item.")
474             .that(selection.getPosition(missingElement))
475             .isEqualTo(-1)
476     }
477 
478     /** Ensures a single preGranted item can be removed and added again. */
479     @Test
testSelectionCanRemoveSinglePreGrantedItemnull480     fun testSelectionCanRemoveSinglePreGrantedItem() =
481         runTest {
482             // mock a test item to return isPreGranted as true.
483             val testItem = SelectionData(id = 999, isPreGrantedParam = true)
484 
485             val selection =
486                 GrantsAwareSelectionImpl<SelectionData>(
487                     scope = backgroundScope,
488                     configuration =
489                     provideTestConfigurationFlow(
490                         scope = backgroundScope,
491                         defaultConfiguration = MULTI_SELECT_CONFIG,
492                     ),
493                     preGrantedItemsCount = 1, // corresponding to testItem
494                 )
495 
496             val emissions = mutableListOf<Set<SelectionData>>()
497             backgroundScope.launch { selection.flow.toList(emissions) }
498 
499             val initialSnapshot = selection.snapshot()
500             // There is only one preGranted item
501             assertWithMessage("Initial Snapshot has an unexpected size")
502                 .that(initialSnapshot)
503                 .hasSize(1)
504 
505             // remove preGranted item
506             selection.remove(testItem)
507 
508             val snapshot = selection.snapshot()
509             advanceTimeBy(100)
510             val flow = emissions.last()
511 
512             assertWithMessage("Deselection should contain test item").that(
513                 selection.getDeselection()
514             ).contains(testItem)
515 
516             assertWithMessage("Snapshot contains the removed item.")
517                 .that(snapshot).doesNotContain(testItem)
518 
519             assertWithMessage("Emitted flow value contains the removed item.")
520                 .that(flow)
521                 .doesNotContain(testItem)
522             assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0)
523 
524             // Now add the preGranted item again and verify that it was removed from deselection.
525             selection.add(testItem)
526 
527             val snapshot2 = selection.snapshot()
528             advanceTimeBy(100)
529             val flow2 = emissions.last()
530             assertWithMessage("Deselection should not contain test item").that(
531                 selection
532                     .getDeselection(),
533             )
534                 .doesNotContain(testItem)
535 
536             assertWithMessage("Snapshot contains the added item.")
537                 .that(snapshot2).contains(testItem)
538             assertWithMessage("Snapshot has an unexpected size").that(snapshot2).hasSize(1)
539 
540             assertWithMessage("Emitted flow value contains the removed item.")
541                 .that(flow2)
542                 .contains(testItem)
543             assertWithMessage("Emitted flow has an unexpected size").that(flow2).hasSize(1)
544         }
545 }