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.navigationbar 18 19 import android.content.ContentProvider 20 import android.content.ContentResolver 21 import android.content.Context 22 import android.content.Intent 23 import android.content.pm.PackageManager 24 import android.os.Build 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.hasClickAction 35 import androidx.compose.ui.test.hasText 36 import androidx.compose.ui.test.junit4.createAndroidComposeRule 37 import androidx.test.filters.SdkSuppress 38 import com.android.photopicker.R 39 import com.android.photopicker.core.ActivityModule 40 import com.android.photopicker.core.ApplicationModule 41 import com.android.photopicker.core.ApplicationOwned 42 import com.android.photopicker.core.Background 43 import com.android.photopicker.core.ConcurrencyModule 44 import com.android.photopicker.core.EmbeddedServiceModule 45 import com.android.photopicker.core.Main 46 import com.android.photopicker.core.ViewModelModule 47 import com.android.photopicker.core.configuration.ConfigurationManager 48 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration 49 import com.android.photopicker.core.configuration.provideTestConfigurationFlow 50 import com.android.photopicker.core.events.Events 51 import com.android.photopicker.core.features.FeatureManager 52 import com.android.photopicker.core.glide.GlideTestRule 53 import com.android.photopicker.core.selection.Selection 54 import com.android.photopicker.data.TestPrefetchDataService 55 import com.android.photopicker.data.model.Media 56 import com.android.photopicker.features.PhotopickerFeatureBaseTest 57 import com.android.photopicker.inject.PhotopickerTestModule 58 import com.android.photopicker.tests.HiltTestActivity 59 import com.android.photopicker.util.test.MockContentProviderWrapper 60 import com.android.photopicker.util.test.whenever 61 import com.android.providers.media.flags.Flags 62 import com.google.common.truth.Truth.assertWithMessage 63 import dagger.Lazy 64 import dagger.Module 65 import dagger.hilt.InstallIn 66 import dagger.hilt.android.testing.BindValue 67 import dagger.hilt.android.testing.HiltAndroidRule 68 import dagger.hilt.android.testing.HiltAndroidTest 69 import dagger.hilt.android.testing.UninstallModules 70 import dagger.hilt.components.SingletonComponent 71 import javax.inject.Inject 72 import kotlinx.coroutines.CoroutineDispatcher 73 import kotlinx.coroutines.CoroutineScope 74 import kotlinx.coroutines.ExperimentalCoroutinesApi 75 import kotlinx.coroutines.test.StandardTestDispatcher 76 import kotlinx.coroutines.test.TestScope 77 import kotlinx.coroutines.test.runTest 78 import org.junit.Before 79 import org.junit.Rule 80 import org.junit.Test 81 import org.mockito.Mock 82 import org.mockito.Mockito.any 83 import org.mockito.MockitoAnnotations 84 85 @UninstallModules( 86 ActivityModule::class, 87 ApplicationModule::class, 88 ConcurrencyModule::class, 89 EmbeddedServiceModule::class, 90 ViewModelModule::class, 91 ) 92 @HiltAndroidTest 93 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) 94 class NavigationBarFeatureTest : PhotopickerFeatureBaseTest() { 95 /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */ 96 @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) 97 @get:Rule(order = 1) 98 val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) 99 @get:Rule(order = 2) val glideRule = GlideTestRule() 100 @get:Rule(order = 3) var setFlagsRule = SetFlagsRule() 101 102 /* Setup dependencies for the UninstallModules for the test class. */ 103 @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() 104 105 val testDispatcher = StandardTestDispatcher() 106 107 /* Overrides for ActivityModule */ 108 val testScope: TestScope = TestScope(testDispatcher) 109 @BindValue @Main val mainScope: CoroutineScope = testScope 110 @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope 111 112 /* Overrides for ViewModelModule */ 113 @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope 114 115 /* Overrides for the ConcurrencyModule */ 116 @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher 117 @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher 118 119 /** 120 * Preview uses Glide for loading images, so we have to mock out the dependencies for Glide 121 * Replace the injected ContentResolver binding in [ApplicationModule] with this test value. 122 */ 123 @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver 124 private lateinit var provider: MockContentProviderWrapper 125 @Mock lateinit var mockContentProvider: ContentProvider 126 127 // Needed for UserMonitor 128 @Mock lateinit var mockUserManager: UserManager 129 @Mock lateinit var mockPackageManager: PackageManager 130 131 @Inject lateinit var mockContext: Context 132 @Inject lateinit var selection: Selection<Media> 133 @Inject lateinit var featureManager: FeatureManager 134 @Inject lateinit var events: Events 135 @Inject override lateinit var configurationManager: Lazy<ConfigurationManager> 136 137 @Before setupnull138 fun setup() { 139 MockitoAnnotations.initMocks(this) 140 141 hiltRule.inject() 142 143 // Stub for MockContentResolver constructor 144 whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() } 145 146 // Stub out the content resolver for Glide 147 val mockContentResolver = MockContentResolver(mockContext) 148 provider = MockContentProviderWrapper(mockContentProvider) 149 mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider) 150 contentResolver = mockContentResolver 151 152 // Return a resource png so that glide actually has something to load 153 whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) { 154 getTestableContext().getResources().openRawResourceFd(R.drawable.android) 155 } 156 setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager) 157 } 158 159 /* Ensures the NavigationBar is drawn with the production registered features. */ 160 @Test testNavigationBarProductionConfignull161 fun testNavigationBarProductionConfig() { 162 assertWithMessage("NavigationBar is not always enabled for TEST_ACTION") 163 .that( 164 NavigationBarFeature.Registration.isEnabled( 165 TestPhotopickerConfiguration.build { 166 action("TEST_ACTION") 167 intent(Intent("TEST_ACTION")) 168 } 169 ) 170 ) 171 .isEqualTo(true) 172 173 assertWithMessage("NavigationBar is not always enabled") 174 .that( 175 NavigationBarFeature.Registration.isEnabled( 176 TestPhotopickerConfiguration.build { 177 action(MediaStore.ACTION_PICK_IMAGES) 178 intent(Intent(MediaStore.ACTION_PICK_IMAGES)) 179 } 180 ) 181 ) 182 .isEqualTo(true) 183 184 assertWithMessage("NavigationBar is not always enabled") 185 .that( 186 NavigationBarFeature.Registration.isEnabled( 187 TestPhotopickerConfiguration.build { 188 action(Intent.ACTION_GET_CONTENT) 189 intent(Intent(Intent.ACTION_GET_CONTENT)) 190 } 191 ) 192 ) 193 .isEqualTo(true) 194 195 assertWithMessage("NavigationBar is not always enabled") 196 .that( 197 NavigationBarFeature.Registration.isEnabled( 198 TestPhotopickerConfiguration.build { 199 action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) 200 intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) 201 callingPackage("com.example.test") 202 callingPackageUid(1234) 203 callingPackageLabel("test_app") 204 } 205 ) 206 ) 207 .isEqualTo(true) 208 } 209 210 /* Verify Navigation Bar contains tabs for both photos and albums grid.*/ 211 @Test testNavigationBarIsVisibleWithFeatureTabsnull212 fun testNavigationBarIsVisibleWithFeatureTabs() { 213 // Explicitly create a new feature manager that uses the same production feature 214 // registrations to ensure this test will fail if the default production behavior changes. 215 featureManager = 216 FeatureManager( 217 registeredFeatures = FeatureManager.KNOWN_FEATURE_REGISTRATIONS, 218 scope = testBackgroundScope, 219 prefetchDataService = TestPrefetchDataService(), 220 configuration = provideTestConfigurationFlow(scope = testBackgroundScope), 221 ) 222 223 val photosGridNavButtonLabel = 224 getTestableContext() 225 .getResources() 226 .getString(R.string.photopicker_photos_nav_button_label) 227 val albumsGridNavButtonLabel = 228 getTestableContext() 229 .getResources() 230 .getString(R.string.photopicker_albums_nav_button_label) 231 232 testScope.runTest { 233 composeTestRule.setContent { 234 callPhotopickerMain( 235 featureManager = featureManager, 236 selection = selection, 237 events = events, 238 ) 239 } 240 241 composeTestRule.waitForIdle() 242 243 // Photos Grid Nav Button and Albums Grid Nav Button 244 composeTestRule 245 .onNode(hasText(photosGridNavButtonLabel)) 246 .assertIsDisplayed() 247 .assert(hasClickAction()) 248 249 composeTestRule 250 .onNode(hasText(albumsGridNavButtonLabel)) 251 .assertIsDisplayed() 252 .assert(hasClickAction()) 253 } 254 } 255 256 /* Verify Navigation Bar when search flag disabled contains tabs for both photos and albums grid.*/ 257 @Test 258 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 259 @DisableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH) testNavigationBar_withSearchFlagDisabled_IsVisibleWithFeatureTabsnull260 fun testNavigationBar_withSearchFlagDisabled_IsVisibleWithFeatureTabs() { 261 val photosGridNavButtonLabel = 262 getTestableContext() 263 .getResources() 264 .getString(R.string.photopicker_photos_nav_button_label) 265 val albumsGridNavButtonLabel = 266 getTestableContext() 267 .getResources() 268 .getString(R.string.photopicker_albums_nav_button_label) 269 270 testScope.runTest { 271 composeTestRule.setContent { 272 callPhotopickerMain( 273 featureManager = featureManager, 274 selection = selection, 275 events = events, 276 ) 277 } 278 279 composeTestRule.waitForIdle() 280 281 // Photos Grid Nav Button and Albums Grid Nav Button 282 composeTestRule 283 .onNode(hasText(photosGridNavButtonLabel)) 284 .assertIsDisplayed() 285 .assert(hasClickAction()) 286 287 composeTestRule 288 .onNode(hasText(albumsGridNavButtonLabel)) 289 .assertIsDisplayed() 290 .assert(hasClickAction()) 291 } 292 } 293 294 /* Verify Navigation Bar when search flag enabled contains tabs for both photos and albums grid.*/ 295 @Test 296 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 297 @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH) testNavigationBar_withSearchFlagEnabled_IsVisibleWithFeatureTabsnull298 fun testNavigationBar_withSearchFlagEnabled_IsVisibleWithFeatureTabs() { 299 val photosGridNavButtonLabel = 300 getTestableContext() 301 .getResources() 302 .getString(R.string.photopicker_photos_nav_button_label) 303 val albumsGridNavButtonLabel = 304 getTestableContext() 305 .getResources() 306 .getString(R.string.photopicker_albums_nav_button_label) 307 308 testScope.runTest { 309 composeTestRule.setContent { 310 callPhotopickerMain( 311 featureManager = featureManager, 312 selection = selection, 313 events = events, 314 ) 315 } 316 317 composeTestRule.waitForIdle() 318 319 // Photos Grid Nav Button and Albums Grid Nav Button 320 composeTestRule 321 .onNode(hasText(photosGridNavButtonLabel)) 322 .assertIsDisplayed() 323 .assert(hasClickAction()) 324 325 composeTestRule 326 .onNode(hasText(albumsGridNavButtonLabel)) 327 .assertIsDisplayed() 328 .assert(hasClickAction()) 329 } 330 } 331 } 332