• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.search
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.os.Build
24 import android.os.UserHandle
25 import android.os.UserManager
26 import android.platform.test.annotations.DisableFlags
27 import android.platform.test.annotations.EnableFlags
28 import android.platform.test.flag.junit.SetFlagsRule
29 import android.provider.MediaStore
30 import android.test.mock.MockContentResolver
31 import androidx.compose.ui.test.ExperimentalTestApi
32 import androidx.compose.ui.test.assert
33 import androidx.compose.ui.test.assertIsDisplayed
34 import androidx.compose.ui.test.assertIsNotDisplayed
35 import androidx.compose.ui.test.hasClickAction
36 import androidx.compose.ui.test.hasContentDescription
37 import androidx.compose.ui.test.hasText
38 import androidx.compose.ui.test.junit4.createAndroidComposeRule
39 import androidx.compose.ui.test.onNodeWithText
40 import androidx.compose.ui.test.performClick
41 import androidx.compose.ui.test.performTextInput
42 import androidx.test.filters.SdkSuppress
43 import com.android.photopicker.R
44 import com.android.photopicker.core.ActivityModule
45 import com.android.photopicker.core.ApplicationModule
46 import com.android.photopicker.core.ApplicationOwned
47 import com.android.photopicker.core.Background
48 import com.android.photopicker.core.ConcurrencyModule
49 import com.android.photopicker.core.EmbeddedServiceModule
50 import com.android.photopicker.core.Main
51 import com.android.photopicker.core.ViewModelModule
52 import com.android.photopicker.core.configuration.ConfigurationManager
53 import com.android.photopicker.core.configuration.PhotopickerConfiguration
54 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
55 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
56 import com.android.photopicker.core.events.Events
57 import com.android.photopicker.core.features.FeatureManager
58 import com.android.photopicker.core.features.PrefetchResultKey
59 import com.android.photopicker.core.selection.Selection
60 import com.android.photopicker.data.model.Media
61 import com.android.photopicker.features.PhotopickerFeatureBaseTest
62 import com.android.photopicker.features.search.model.GlobalSearchState
63 import com.android.photopicker.inject.PhotopickerTestModule
64 import com.android.photopicker.tests.HiltTestActivity
65 import com.android.providers.media.flags.Flags
66 import com.google.common.truth.Truth.assertWithMessage
67 import dagger.Lazy
68 import dagger.Module
69 import dagger.hilt.InstallIn
70 import dagger.hilt.android.testing.BindValue
71 import dagger.hilt.android.testing.HiltAndroidRule
72 import dagger.hilt.android.testing.HiltAndroidTest
73 import dagger.hilt.android.testing.UninstallModules
74 import dagger.hilt.components.SingletonComponent
75 import javax.inject.Inject
76 import kotlinx.coroutines.CoroutineDispatcher
77 import kotlinx.coroutines.CoroutineScope
78 import kotlinx.coroutines.Deferred
79 import kotlinx.coroutines.ExperimentalCoroutinesApi
80 import kotlinx.coroutines.async
81 import kotlinx.coroutines.runBlocking
82 import kotlinx.coroutines.test.StandardTestDispatcher
83 import kotlinx.coroutines.test.TestScope
84 import kotlinx.coroutines.test.advanceTimeBy
85 import kotlinx.coroutines.test.runTest
86 import org.junit.Before
87 import org.junit.Rule
88 import org.junit.Test
89 import org.mockito.Mock
90 import org.mockito.MockitoAnnotations
91 
92 @UninstallModules(
93     ActivityModule::class,
94     ApplicationModule::class,
95     ConcurrencyModule::class,
96     EmbeddedServiceModule::class,
97     ViewModelModule::class,
98 )
99 @HiltAndroidTest
100 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
101 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
102 class SearchFeatureTest : PhotopickerFeatureBaseTest() {
103     /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
104     @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
105     @get:Rule(order = 1)
106     val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
107     @get:Rule(order = 2) var setFlagsRule = SetFlagsRule()
108 
109     /* Setup dependencies for the UninstallModules for the test class. */
110     @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
111 
112     val testDispatcher = StandardTestDispatcher()
113 
114     /* Overrides for ActivityModule */
115     val testScope: TestScope = TestScope(testDispatcher)
116     @BindValue @Main val mainScope: CoroutineScope = testScope
117     @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
118 
119     /* Overrides for ViewModelModule */
120     @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
121 
122     /* Overrides for the ConcurrencyModule */
123     @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
124     @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
125 
126     @Inject lateinit var events: Events
127     @Inject lateinit var selection: Selection<Media>
128     @Inject lateinit var featureManager: FeatureManager
129     @Inject lateinit var userHandle: UserHandle
130     @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
131 
132     @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
133 
134     @Inject lateinit var mockContext: Context
135     @Mock lateinit var mockUserManager: UserManager
136     @Mock lateinit var mockPackageManager: PackageManager
137 
138     val deferredPrefetchResultsMap: Map<PrefetchResultKey, Deferred<Any?>> =
139         mapOf(
140             PrefetchResultKey.SEARCH_STATE to
<lambda>null141                 runBlocking {
142                     async {
143                         return@async GlobalSearchState.ENABLED
144                     }
145                 }
146         )
147 
148     @Before
setupnull149     fun setup() {
150 
151         MockitoAnnotations.initMocks(this)
152         hiltRule.inject()
153         setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
154     }
155 
156     /* Ensures the Search feature is not enabled when flag is disabled. */
157     @Test
158     @DisableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchFeature_whenFlagDisabled_isNotEnablednull159     fun testSearchFeature_whenFlagDisabled_isNotEnabled() {
160         val testActionPickImagesConfiguration: PhotopickerConfiguration =
161             TestPhotopickerConfiguration.build {
162                 action(MediaStore.ACTION_PICK_IMAGES)
163                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
164             }
165         assertWithMessage("SearchBar is always enabled when search flag is disabled")
166             .that(
167                 SearchFeature.Registration.isEnabled(
168                     testActionPickImagesConfiguration,
169                     deferredPrefetchResultsMap,
170                 )
171             )
172             .isEqualTo(false)
173 
174         val testGetContentConfiguration: PhotopickerConfiguration =
175             TestPhotopickerConfiguration.build {
176                 action(Intent.ACTION_GET_CONTENT)
177                 intent(Intent(Intent.ACTION_GET_CONTENT))
178             }
179         assertWithMessage("Search Feature is always enabled when search flag is disabled")
180             .that(
181                 SearchFeature.Registration.isEnabled(
182                     testGetContentConfiguration,
183                     deferredPrefetchResultsMap,
184                 )
185             )
186             .isEqualTo(false)
187 
188         val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
189             TestPhotopickerConfiguration.build {
190                 action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
191                 intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
192                 callingPackage("com.example.test")
193                 callingPackageUid(1234)
194                 callingPackageLabel("test_app")
195             }
196         assertWithMessage("Search Feature is always enabled when search flag is disabled")
197             .that(
198                 SearchFeature.Registration.isEnabled(
199                     testUserSelectImagesForAppConfiguration,
200                     deferredPrefetchResultsMap,
201                 )
202             )
203             .isEqualTo(false)
204     }
205 
206     /* Verify Search feature is enabled when Search flag enabled.*/
207     @Test
208     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchFeature_whenFlagEnabled_isEnablednull209     fun testSearchFeature_whenFlagEnabled_isEnabled() {
210         val testActionPickImagesConfiguration: PhotopickerConfiguration =
211             TestPhotopickerConfiguration.build {
212                 action(MediaStore.ACTION_PICK_IMAGES)
213                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
214             }
215         assertWithMessage("Search Feature is not always enabled when search flag enabled")
216             .that(
217                 SearchFeature.Registration.isEnabled(
218                     testActionPickImagesConfiguration,
219                     deferredPrefetchResultsMap,
220                 )
221             )
222             .isEqualTo(true)
223 
224         val testGetContentConfiguration: PhotopickerConfiguration =
225             TestPhotopickerConfiguration.build {
226                 action(Intent.ACTION_GET_CONTENT)
227                 intent(Intent(Intent.ACTION_GET_CONTENT))
228             }
229         assertWithMessage("Search Feature is not always enabled when search flag enabled")
230             .that(
231                 SearchFeature.Registration.isEnabled(
232                     testGetContentConfiguration,
233                     deferredPrefetchResultsMap,
234                 )
235             )
236             .isEqualTo(true)
237     }
238 
239     /* Verify Search feature is enabled when Search flag and Embedded picker is enabled.*/
240     @Test
241     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH, Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER)
testSearchFeature_whenEmbeddedPickerEnabled_isEnablednull242     fun testSearchFeature_whenEmbeddedPickerEnabled_isEnabled() {
243         val testActionPickImagesConfiguration: PhotopickerConfiguration =
244             TestPhotopickerConfiguration.build {
245                 runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
246                 action(MediaStore.ACTION_PICK_IMAGES)
247                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
248             }
249         assertWithMessage("Search Feature is not always enabled when search flag enabled")
250             .that(
251                 SearchFeature.Registration.isEnabled(
252                     testActionPickImagesConfiguration,
253                     deferredPrefetchResultsMap,
254                 )
255             )
256             .isEqualTo(true)
257 
258         val testGetContentConfiguration: PhotopickerConfiguration =
259             TestPhotopickerConfiguration.build {
260                 runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
261                 action(Intent.ACTION_GET_CONTENT)
262                 intent(Intent(Intent.ACTION_GET_CONTENT))
263             }
264         assertWithMessage("Search Feature is not always enabled when search flag enabled")
265             .that(
266                 SearchFeature.Registration.isEnabled(
267                     testGetContentConfiguration,
268                     deferredPrefetchResultsMap,
269                 )
270             )
271             .isEqualTo(true)
272     }
273 
274     @Test
275     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchFeature_inPermissionMode_isDisablednull276     fun testSearchFeature_inPermissionMode_isDisabled() {
277         val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
278             TestPhotopickerConfiguration.build {
279                 action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
280                 intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
281                 callingPackage("com.example.test")
282                 callingPackageUid(1234)
283                 callingPackageLabel("test_app")
284             }
285         assertWithMessage("Search Feature is always enabled in Permission mode")
286             .that(
287                 SearchFeature.Registration.isEnabled(
288                     testUserSelectImagesForAppConfiguration,
289                     deferredPrefetchResultsMap,
290                 )
291             )
292             .isEqualTo(false)
293     }
294 
295     @Test
296     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH, Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER)
testSearchFeature_whenEmbeddedPickerEnabledInPermissionMode_isDisablednull297     fun testSearchFeature_whenEmbeddedPickerEnabledInPermissionMode_isDisabled() {
298         val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
299             TestPhotopickerConfiguration.build {
300                 runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
301                 action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
302                 intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
303                 callingPackage("com.example.test")
304                 callingPackageUid(1234)
305                 callingPackageLabel("test_app")
306             }
307         assertWithMessage("Search Feature in embedded picker is always enabled in Perission mode")
308             .that(
309                 SearchFeature.Registration.isEnabled(
310                     testUserSelectImagesForAppConfiguration,
311                     deferredPrefetchResultsMap,
312                 )
313             )
314             .isEqualTo(false)
315     }
316 
317     @Test
318     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_whenFlagEnabled_isDisplayednull319     fun testSearchBar_whenFlagEnabled_isDisplayed() =
320         testScope.runTest {
321             val resources = getTestableContext().getResources()
322             composeTestRule.setContent {
323                 callPhotopickerMain(
324                     featureManager = featureManager,
325                     selection = selection,
326                     events = events,
327                 )
328             }
329             composeTestRule
330                 .onNode(
331                     hasText(
332                         getTestableContext()
333                             .getResources()
334                             .getString(R.string.photopicker_search_placeholder_text)
335                     )
336                 )
337                 .assertIsDisplayed()
338             composeTestRule.onNode(
339                 hasContentDescription(
340                     resources.getString(R.string.photopicker_search_placeholder_text)
341                 )
342             )
343         }
344 
345     @Test
346     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_whenClicked_opensSearchViewWithBackActionnull347     fun testSearchBar_whenClicked_opensSearchViewWithBackAction() =
348         testScope.runTest {
349             val resources = getTestableContext().getResources()
350             composeTestRule.setContent {
351                 callPhotopickerMain(
352                     featureManager = featureManager,
353                     selection = selection,
354                     events = events,
355                 )
356             }
357 
358             // Perform click action on the Search bar
359             composeTestRule
360                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
361                 .assertIsDisplayed()
362                 .performClick()
363             composeTestRule.waitForIdle()
364             advanceTimeBy(1000)
365 
366             // Asserts search view page with its placeholder text displayed
367             composeTestRule
368                 .onNode(
369                     hasText(
370                         resources.getString(R.string.photopicker_search_photos_placeholder_text)
371                     )
372                 )
373                 .assertIsDisplayed()
374 
375             // Perform click action on back button in search bar of search view page
376             composeTestRule
377                 .onNode(
378                     hasContentDescription(resources.getString(R.string.photopicker_back_option))
379                 )
380                 .assert(hasClickAction())
381                 .performClick()
382             composeTestRule.waitForIdle()
383             advanceTimeBy(1000)
384 
385             // Search bar with Search text placeholder is displayed
386             composeTestRule
387                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
388                 .assertIsDisplayed()
389         }
390 
391     @Test
392     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_onBackAction_clearsQuerynull393     fun testSearchBar_onBackAction_clearsQuery() =
394         testScope.runTest {
395             val resources = getTestableContext().getResources()
396             composeTestRule.setContent {
397                 callPhotopickerMain(
398                     featureManager = featureManager,
399                     selection = selection,
400                     events = events,
401                 )
402             }
403 
404             // Perform click action on the Search bar
405             composeTestRule
406                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
407                 .performClick()
408             composeTestRule.waitForIdle()
409             advanceTimeBy(1000)
410 
411             // Input test query in search bar and verify it is displayed
412             val testQuery = "testquery"
413             composeTestRule
414                 .onNode(
415                     hasText(
416                         resources.getString(R.string.photopicker_search_photos_placeholder_text)
417                     )
418                 )
419                 .performTextInput(testQuery)
420 
421             composeTestRule.onNodeWithText(testQuery).assertIsDisplayed()
422 
423             // Perform click action on back button in search bar of search view page
424             composeTestRule
425                 .onNode(
426                     hasContentDescription(resources.getString(R.string.photopicker_back_option))
427                 )
428                 .performClick()
429             composeTestRule.waitForIdle()
430             advanceTimeBy(1000)
431 
432             // Make sure test query is cleared and Search text placeholder is displayed
433             composeTestRule.onNodeWithText(testQuery).assertIsNotDisplayed()
434             composeTestRule
435                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
436                 .assertIsDisplayed()
437         }
438 
439     @Test
440     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_mimetypeOnlyVideo_showsVideoPlaceHolderTextnull441     fun testSearchBar_mimetypeOnlyVideo_showsVideoPlaceHolderText() =
442         testScope.runTest {
443             val resources = getTestableContext().getResources()
444             val testIntent =
445                 Intent(MediaStore.ACTION_PICK_IMAGES).apply {
446                     putExtra(Intent.EXTRA_MIME_TYPES, arrayListOf("video/*", "video/mpeg"))
447                 }
448             configurationManager.get().setIntent(testIntent)
449 
450             composeTestRule.setContent {
451                 callPhotopickerMain(
452                     featureManager = featureManager,
453                     selection = selection,
454                     events = events,
455                 )
456             }
457 
458             // Perform click action on the Search bar
459             composeTestRule
460                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
461                 .performClick()
462             composeTestRule.waitForIdle()
463 
464             composeTestRule
465                 .onNode(
466                     hasText(
467                         resources.getString(R.string.photopicker_search_videos_placeholder_text)
468                     )
469                 )
470                 .assertIsDisplayed()
471         }
472 
473     @Test
474     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_mimetypeOnlyImage_showsPhotosPlaceHolderTextnull475     fun testSearchBar_mimetypeOnlyImage_showsPhotosPlaceHolderText() =
476         testScope.runTest {
477             val resources = getTestableContext().getResources()
478             val testIntent =
479                 Intent(MediaStore.ACTION_PICK_IMAGES).apply {
480                     putExtra(Intent.EXTRA_MIME_TYPES, arrayListOf("image/*", "image/png"))
481                 }
482             configurationManager.get().setIntent(testIntent)
483             composeTestRule.setContent {
484                 callPhotopickerMain(
485                     featureManager = featureManager,
486                     selection = selection,
487                     events = events,
488                 )
489             }
490 
491             // Perform click action on the Search bar
492             composeTestRule
493                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
494                 .performClick()
495             composeTestRule.waitForIdle()
496 
497             composeTestRule
498                 .onNode(
499                     hasText(
500                         resources.getString(R.string.photopicker_search_photos_placeholder_text)
501                     )
502                 )
503                 .assertIsDisplayed()
504         }
505 
506     @Test
507     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_mimetypeImageAndVideo_showsPhotosPlaceHolderTextnull508     fun testSearchBar_mimetypeImageAndVideo_showsPhotosPlaceHolderText() =
509         testScope.runTest {
510             val resources = getTestableContext().getResources()
511             val testIntent =
512                 Intent(MediaStore.ACTION_PICK_IMAGES).apply {
513                     putExtra(Intent.EXTRA_MIME_TYPES, arrayListOf("image/*", "video/*"))
514                 }
515             configurationManager.get().setIntent(testIntent)
516             composeTestRule.setContent {
517                 callPhotopickerMain(
518                     featureManager = featureManager,
519                     selection = selection,
520                     events = events,
521                 )
522             }
523 
524             // Perform click action on the Search bar
525             composeTestRule
526                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
527                 .performClick()
528             composeTestRule.waitForIdle()
529 
530             composeTestRule
531                 .onNode(
532                     hasText(
533                         resources.getString(R.string.photopicker_search_photos_placeholder_text)
534                     )
535                 )
536                 .assertIsDisplayed()
537         }
538 
539     @Test
540     @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
testSearchBar_mimeTypeAll_showsPhotosPlaceHolderTextnull541     fun testSearchBar_mimeTypeAll_showsPhotosPlaceHolderText() =
542         testScope.runTest {
543             val resources = getTestableContext().getResources()
544             val testIntent =
545                 Intent(MediaStore.ACTION_PICK_IMAGES).apply {
546                     putExtra(Intent.EXTRA_MIME_TYPES, arrayListOf("*/*"))
547                 }
548             configurationManager.get().setIntent(testIntent)
549             composeTestRule.setContent {
550                 callPhotopickerMain(
551                     featureManager = featureManager,
552                     selection = selection,
553                     events = events,
554                 )
555             }
556 
557             // Perform click action on the Search bar
558             composeTestRule
559                 .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
560                 .performClick()
561             composeTestRule.waitForIdle()
562 
563             composeTestRule
564                 .onNode(
565                     hasText(
566                         resources.getString(R.string.photopicker_search_photos_placeholder_text)
567                     )
568                 )
569                 .assertIsDisplayed()
570         }
571 }
572