1 /*
2  * Copyright (C) 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.features.selectionbar
18 
19 import android.content.ContentResolver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.pm.PackageManager
23 import android.net.Uri
24 import android.os.UserManager
25 import android.provider.MediaStore
26 import android.test.mock.MockContentResolver
27 import androidx.compose.runtime.CompositionLocalProvider
28 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.platform.testTag
30 import androidx.compose.ui.test.ExperimentalTestApi
31 import androidx.compose.ui.test.assert
32 import androidx.compose.ui.test.assertIsDisplayed
33 import androidx.compose.ui.test.hasClickAction
34 import androidx.compose.ui.test.hasContentDescription
35 import androidx.compose.ui.test.hasTestTag
36 import androidx.compose.ui.test.hasText
37 import androidx.compose.ui.test.junit4.createAndroidComposeRule
38 import androidx.compose.ui.test.performClick
39 import com.android.photopicker.R
40 import com.android.photopicker.core.ActivityModule
41 import com.android.photopicker.core.ApplicationModule
42 import com.android.photopicker.core.ApplicationOwned
43 import com.android.photopicker.core.Background
44 import com.android.photopicker.core.ConcurrencyModule
45 import com.android.photopicker.core.EmbeddedServiceModule
46 import com.android.photopicker.core.Main
47 import com.android.photopicker.core.configuration.ConfigurationManager
48 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
49 import com.android.photopicker.core.configuration.PhotopickerConfiguration
50 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
51 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
52 import com.android.photopicker.core.configuration.provideTestConfigurationFlow
53 import com.android.photopicker.core.events.Events
54 import com.android.photopicker.core.events.LocalEvents
55 import com.android.photopicker.core.events.generatePickerSessionId
56 import com.android.photopicker.core.features.FeatureManager
57 import com.android.photopicker.core.features.LocalFeatureManager
58 import com.android.photopicker.core.features.LocationParams
59 import com.android.photopicker.core.glide.GlideTestRule
60 import com.android.photopicker.core.navigation.LocalNavController
61 import com.android.photopicker.core.selection.LocalSelection
62 import com.android.photopicker.core.selection.Selection
63 import com.android.photopicker.core.theme.PhotopickerTheme
64 import com.android.photopicker.data.TestPrefetchDataService
65 import com.android.photopicker.data.model.Media
66 import com.android.photopicker.data.model.MediaSource
67 import com.android.photopicker.features.PhotopickerFeatureBaseTest
68 import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
69 import com.android.photopicker.inject.PhotopickerTestModule
70 import com.android.photopicker.tests.HiltTestActivity
71 import com.android.photopicker.util.test.whenever
72 import com.google.common.truth.Truth.assertWithMessage
73 import dagger.Lazy
74 import dagger.Module
75 import dagger.hilt.InstallIn
76 import dagger.hilt.android.testing.BindValue
77 import dagger.hilt.android.testing.HiltAndroidRule
78 import dagger.hilt.android.testing.HiltAndroidTest
79 import dagger.hilt.android.testing.UninstallModules
80 import dagger.hilt.components.SingletonComponent
81 import javax.inject.Inject
82 import kotlinx.coroutines.CompletableDeferred
83 import kotlinx.coroutines.CoroutineDispatcher
84 import kotlinx.coroutines.CoroutineScope
85 import kotlinx.coroutines.ExperimentalCoroutinesApi
86 import kotlinx.coroutines.test.StandardTestDispatcher
87 import kotlinx.coroutines.test.TestScope
88 import kotlinx.coroutines.test.advanceTimeBy
89 import kotlinx.coroutines.test.runTest
90 import org.junit.Before
91 import org.junit.Rule
92 import org.junit.Test
93 import org.mockito.Mock
94 import org.mockito.MockitoAnnotations
95 
96 @UninstallModules(
97     ActivityModule::class,
98     ApplicationModule::class,
99     ConcurrencyModule::class,
100     EmbeddedServiceModule::class,
101 )
102 @HiltAndroidTest
103 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
104 class SelectionBarFeatureTest : PhotopickerFeatureBaseTest() {
105 
106     /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
107     @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
108     @get:Rule(order = 1)
109     val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
110     @get:Rule(order = 2) val glideRule = GlideTestRule()
111 
112     /* Setup dependencies for the UninstallModules for the test class. */
113     @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
114 
115     val testDispatcher = StandardTestDispatcher()
116     val sessionId = generatePickerSessionId()
117 
118     /* Overrides for ActivityModule */
119     val testScope: TestScope = TestScope(testDispatcher)
120     @BindValue @Main val mainScope: CoroutineScope = testScope
121     @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
122 
123     /* Overrides for the ConcurrencyModule */
124     @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
125     @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
126 
127     @Mock lateinit var mockUserManager: UserManager
128     @Mock lateinit var mockPackageManager: PackageManager
129     @BindValue @ApplicationOwned lateinit var mockContentResolver: ContentResolver
130 
131     @Inject lateinit var mockContext: Context
132     @Inject lateinit var selection: Lazy<Selection<Media>>
133     @Inject lateinit var featureManager: Lazy<FeatureManager>
134     @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
135     @Inject lateinit var events: Lazy<Events>
136 
137     val TEST_TAG_SELECTION_BAR = "selection_bar"
138     val MEDIA_ITEM =
139         Media.Image(
140             mediaId = "1",
141             pickerId = 1L,
142             authority = "a",
143             mediaSource = MediaSource.LOCAL,
144             mediaUri =
145                 Uri.EMPTY.buildUpon()
<lambda>null146                     .apply {
147                         scheme("content")
148                         authority("media")
149                         path("picker")
150                         path("a")
151                         path("1")
152                     }
153                     .build(),
154             glideLoadableUri =
155                 Uri.EMPTY.buildUpon()
<lambda>null156                     .apply {
157                         scheme("content")
158                         authority("a")
159                         path("1")
160                     }
161                     .build(),
162             dateTakenMillisLong = 123456789L,
163             sizeInBytes = 1000L,
164             mimeType = "image/png",
165             standardMimeTypeExtension = 1,
166         )
167 
168     @Before
setupnull169     fun setup() {
170         MockitoAnnotations.initMocks(this)
171         hiltRule.inject()
172 
173         val testIntent =
174             Intent(MediaStore.ACTION_PICK_IMAGES).apply {
175                 putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 5)
176             }
177         configurationManager.get().setIntent(testIntent)
178 
179         // Stub for MockContentResolver constructor
180         whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
181         mockContentResolver = MockContentResolver(mockContext)
182 
183         setupTestForUserMonitor(
184             mockContext,
185             mockUserManager,
186             mockContentResolver,
187             mockPackageManager,
188         )
189     }
190 
191     @Test
testSelectionBarIsEnabledWithSelectionLimitInActivityModenull192     fun testSelectionBarIsEnabledWithSelectionLimitInActivityMode() {
193         val configOne =
194             PhotopickerConfiguration(
195                 action = "TEST_ACTION",
196                 selectionLimit = 5,
197                 sessionId = sessionId,
198             )
199         assertWithMessage("SelectionBarFeature is not always enabled for TEST_ACTION")
200             .that(SelectionBarFeature.Registration.isEnabled(configOne))
201             .isEqualTo(true)
202 
203         val configTwo =
204             PhotopickerConfiguration(
205                 action = MediaStore.ACTION_PICK_IMAGES,
206                 selectionLimit = 5,
207                 sessionId = sessionId,
208             )
209         assertWithMessage("SelectionBarFeature is not always enabled")
210             .that(SelectionBarFeature.Registration.isEnabled(configTwo))
211             .isEqualTo(true)
212 
213         val configThree =
214             PhotopickerConfiguration(
215                 action = Intent.ACTION_GET_CONTENT,
216                 selectionLimit = 5,
217                 sessionId = sessionId,
218             )
219         assertWithMessage("SelectionBarFeature is not always enabled")
220             .that(SelectionBarFeature.Registration.isEnabled(configThree))
221             .isEqualTo(true)
222     }
223 
224     @Test
testSelectionBarNotEnabledForSingleSelectInActivityModenull225     fun testSelectionBarNotEnabledForSingleSelectInActivityMode() {
226         val configOne = PhotopickerConfiguration(action = "TEST_ACTION", sessionId = sessionId)
227         assertWithMessage("SelectionBarFeature is not always enabled for TEST_ACTION")
228             .that(SelectionBarFeature.Registration.isEnabled(configOne))
229             .isEqualTo(false)
230 
231         val configTwo =
232             PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, sessionId = sessionId)
233         assertWithMessage("SelectionBarFeature is not always enabled")
234             .that(SelectionBarFeature.Registration.isEnabled(configTwo))
235             .isEqualTo(false)
236 
237         val configThree =
238             PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, sessionId = sessionId)
239         assertWithMessage("SelectionBarFeature is not always enabled")
240             .that(SelectionBarFeature.Registration.isEnabled(configThree))
241             .isEqualTo(false)
242     }
243 
244     @Test
testSelectionBarIsAlwaysEnabledInEmbeddedModenull245     fun testSelectionBarIsAlwaysEnabledInEmbeddedMode() {
246         val configOne =
247             PhotopickerConfiguration(
248                 action = "",
249                 runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
250                 selectionLimit = 1,
251                 sessionId = sessionId,
252             )
253         assertWithMessage("SelectionBarFeature not always enabled for EMBEDDED mode")
254             .that(SelectionBarFeature.Registration.isEnabled(configOne))
255             .isEqualTo(true)
256 
257         val configTwo =
258             PhotopickerConfiguration(
259                 action = "",
260                 runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
261                 selectionLimit = 20,
262                 sessionId = sessionId,
263             )
264         assertWithMessage("SelectionBarFeature not always enabled for EMBEDDED mode")
265             .that(SelectionBarFeature.Registration.isEnabled(configTwo))
266             .isEqualTo(true)
267     }
268 
269     @Test
testSelectionBarIsShownnull270     fun testSelectionBarIsShown() {
271         testScope.runTest {
272             val photopickerConfiguration: PhotopickerConfiguration =
273                 TestPhotopickerConfiguration.build {
274                     action("TEST_ACTION")
275                     intent(Intent("TEST_ACTION"))
276                 }
277             composeTestRule.setContent {
278                 CompositionLocalProvider(
279                     LocalFeatureManager provides featureManager.get(),
280                     LocalSelection provides selection.get(),
281                     LocalEvents provides events.get(),
282                     LocalNavController provides createNavController(),
283                     LocalPhotopickerConfiguration provides photopickerConfiguration,
284                 ) {
285                     PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
286                         SelectionBar(
287                             modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
288                             params = LocationParams.None,
289                         )
290                     }
291                 }
292             }
293             composeTestRule.onNode(hasTestTag(TEST_TAG_SELECTION_BAR)).assertDoesNotExist()
294             selection.get().add(MEDIA_ITEM)
295             advanceTimeBy(100)
296             composeTestRule.waitForIdle()
297 
298             composeTestRule
299                 .onNode(hasTestTag(TEST_TAG_SELECTION_BAR))
300                 .assertExists()
301                 .assertIsDisplayed()
302         }
303     }
304 
305     @Test
testSelectionBarIsAlwaysShownForGrantsAwareSelectionnull306     fun testSelectionBarIsAlwaysShownForGrantsAwareSelection() {
307         testScope.runTest {
308             val photopickerConfiguration: PhotopickerConfiguration =
309                 TestPhotopickerConfiguration.build {
310                     action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
311                     intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
312                     callingPackage("com.example.test")
313                     callingPackageUid(1234)
314                     callingPackageLabel("test_app")
315                 }
316             composeTestRule.setContent {
317                 CompositionLocalProvider(
318                     LocalFeatureManager provides featureManager.get(),
319                     LocalSelection provides selection.get(),
320                     LocalEvents provides events.get(),
321                     LocalNavController provides createNavController(),
322                     LocalPhotopickerConfiguration provides photopickerConfiguration,
323                 ) {
324                     PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
325                         SelectionBar(
326                             modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
327                             params = LocationParams.None,
328                         )
329                     }
330                 }
331             }
332             composeTestRule.waitForIdle()
333 
334             // verify that the selection bar is displayed
335             composeTestRule
336                 .onNode(hasTestTag(TEST_TAG_SELECTION_BAR))
337                 .assertExists()
338                 .assertIsDisplayed()
339         }
340     }
341 
342     @Test
testSelectionBarShowsSecondaryActionnull343     fun testSelectionBarShowsSecondaryAction() {
344         val testFeatureRegistrations =
345             setOf(SelectionBarFeature.Registration, SimpleUiFeature.Registration)
346 
347         testScope.runTest {
348             val testFeatureManager =
349                 FeatureManager(
350                     provideTestConfigurationFlow(scope = this.backgroundScope),
351                     this.backgroundScope,
352                     TestPrefetchDataService(),
353                     testFeatureRegistrations,
354                 )
355             val photopickerConfiguration: PhotopickerConfiguration =
356                 TestPhotopickerConfiguration.build {
357                     action("TEST_ACTION")
358                     intent(Intent("TEST_ACTION"))
359                 }
360             composeTestRule.setContent {
361                 CompositionLocalProvider(
362                     LocalFeatureManager provides testFeatureManager,
363                     LocalSelection provides selection.get(),
364                     LocalEvents provides events.get(),
365                     LocalPhotopickerConfiguration provides photopickerConfiguration,
366                 ) {
367                     PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
368                         SelectionBar(
369                             modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
370                             params = LocationParams.None,
371                         )
372                     }
373                 }
374             }
375 
376             composeTestRule.onNode(hasText(SimpleUiFeature.BUTTON_LABEL)).assertDoesNotExist()
377             selection.get().add(MEDIA_ITEM)
378             advanceTimeBy(100)
379             composeTestRule.waitForIdle()
380 
381             composeTestRule
382                 .onNode(hasText(SimpleUiFeature.BUTTON_LABEL))
383                 .assertExists()
384                 .assertIsDisplayed()
385         }
386     }
387 
388     @Test
testSelectionBarPrimaryActionnull389     fun testSelectionBarPrimaryAction() {
390 
391         testScope.runTest {
392             val clicked = CompletableDeferred<Boolean>()
393             val photopickerConfiguration: PhotopickerConfiguration =
394                 TestPhotopickerConfiguration.build {
395                     action("TEST_ACTION")
396                     intent(Intent("TEST_ACTION"))
397                 }
398             composeTestRule.setContent {
399                 CompositionLocalProvider(
400                     LocalFeatureManager provides featureManager.get(),
401                     LocalSelection provides selection.get(),
402                     LocalEvents provides events.get(),
403                     LocalNavController provides createNavController(),
404                     LocalPhotopickerConfiguration provides photopickerConfiguration,
405                 ) {
406                     PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
407                         SelectionBar(
408                             modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
409                             params = LocationParams.WithClickAction { clicked.complete(true) },
410                         )
411                     }
412                 }
413             }
414 
415             // Populate selection with an item, and wait for animations to complete.
416             selection.get().add(MEDIA_ITEM)
417             advanceTimeBy(100)
418             composeTestRule.waitForIdle()
419 
420             val resources = getTestableContext().getResources()
421             val buttonLabel = resources.getString(R.string.photopicker_done_button_label)
422 
423             // Find the button, ensure it has a registered click handler, is displayed.
424             composeTestRule
425                 .onNode(hasText(buttonLabel))
426                 .assertIsDisplayed()
427                 .assert(hasClickAction())
428                 .performClick()
429 
430             val wasClicked = clicked.await()
431             assertWithMessage("Expected primary action to invoke click handler")
432                 .that(wasClicked)
433                 .isTrue()
434         }
435     }
436 
437     @Test
testSelectionBarClearSelectionnull438     fun testSelectionBarClearSelection() {
439 
440         testScope.runTest {
441             val photopickerConfiguration: PhotopickerConfiguration =
442                 TestPhotopickerConfiguration.build {
443                     action("TEST_ACTION")
444                     intent(Intent("TEST_ACTION"))
445                 }
446 
447             composeTestRule.setContent {
448                 CompositionLocalProvider(
449                     LocalFeatureManager provides featureManager.get(),
450                     LocalSelection provides selection.get(),
451                     LocalEvents provides events.get(),
452                     LocalNavController provides createNavController(),
453                     LocalPhotopickerConfiguration provides photopickerConfiguration,
454                 ) {
455                     PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
456                         SelectionBar(
457                             modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
458                             params = LocationParams.None,
459                         )
460                     }
461                 }
462             }
463 
464             // Populate selection with an item, and wait for animations to complete.
465             selection.get().add(MEDIA_ITEM)
466 
467             assertWithMessage("Expected selection to contain an item.")
468                 .that(selection.get().snapshot().size)
469                 .isEqualTo(1)
470 
471             advanceTimeBy(100)
472             composeTestRule.waitForIdle()
473 
474             val resources = getTestableContext().getResources()
475             val clearDescription =
476                 resources.getString(R.string.photopicker_clear_selection_button_description)
477 
478             // Find the button, ensure it has a registered click handler, is displayed.
479             composeTestRule
480                 .onNode(hasContentDescription(clearDescription))
481                 .assertIsDisplayed()
482                 .assert(hasClickAction())
483                 .performClick()
484 
485             advanceTimeBy(100)
486 
487             assertWithMessage("Expected selection to be cleared.")
488                 .that(selection.get().snapshot())
489                 .isEmpty()
490         }
491     }
492 }
493