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