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.browse 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.UserManager 24 import android.provider.MediaStore 25 import android.test.mock.MockContentResolver 26 import androidx.compose.ui.test.ExperimentalTestApi 27 import androidx.compose.ui.test.assertIsDisplayed 28 import androidx.compose.ui.test.hasContentDescription 29 import androidx.compose.ui.test.hasText 30 import androidx.compose.ui.test.junit4.createAndroidComposeRule 31 import androidx.compose.ui.test.performClick 32 import com.android.photopicker.R 33 import com.android.photopicker.core.ActivityModule 34 import com.android.photopicker.core.ApplicationModule 35 import com.android.photopicker.core.ApplicationOwned 36 import com.android.photopicker.core.Background 37 import com.android.photopicker.core.ConcurrencyModule 38 import com.android.photopicker.core.EmbeddedServiceModule 39 import com.android.photopicker.core.Main 40 import com.android.photopicker.core.configuration.ConfigurationManager 41 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv 42 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration 43 import com.android.photopicker.core.events.Event 44 import com.android.photopicker.core.events.Events 45 import com.android.photopicker.core.features.FeatureManager 46 import com.android.photopicker.core.features.FeatureToken 47 import com.android.photopicker.core.glide.GlideTestRule 48 import com.android.photopicker.core.selection.Selection 49 import com.android.photopicker.data.model.Media 50 import com.android.photopicker.features.PhotopickerFeatureBaseTest 51 import com.android.photopicker.features.overflowmenu.OverflowMenuFeature 52 import com.android.photopicker.inject.PhotopickerTestModule 53 import com.android.photopicker.tests.HiltTestActivity 54 import com.google.common.truth.Truth.assertWithMessage 55 import dagger.Lazy 56 import dagger.Module 57 import dagger.hilt.InstallIn 58 import dagger.hilt.android.testing.BindValue 59 import dagger.hilt.android.testing.HiltAndroidRule 60 import dagger.hilt.android.testing.HiltAndroidTest 61 import dagger.hilt.android.testing.UninstallModules 62 import dagger.hilt.components.SingletonComponent 63 import javax.inject.Inject 64 import kotlinx.coroutines.CoroutineDispatcher 65 import kotlinx.coroutines.CoroutineScope 66 import kotlinx.coroutines.ExperimentalCoroutinesApi 67 import kotlinx.coroutines.flow.toList 68 import kotlinx.coroutines.launch 69 import kotlinx.coroutines.test.StandardTestDispatcher 70 import kotlinx.coroutines.test.TestScope 71 import kotlinx.coroutines.test.advanceTimeBy 72 import kotlinx.coroutines.test.runTest 73 import org.junit.Before 74 import org.junit.Rule 75 import org.junit.Test 76 import org.mockito.Mock 77 import org.mockito.MockitoAnnotations 78 79 @UninstallModules( 80 ActivityModule::class, 81 ApplicationModule::class, 82 ConcurrencyModule::class, 83 EmbeddedServiceModule::class, 84 ) 85 @HiltAndroidTest 86 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) 87 class BrowseFeatureTest : PhotopickerFeatureBaseTest() { 88 89 /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */ 90 @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) 91 @get:Rule(order = 1) 92 val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java) 93 @get:Rule(order = 2) val glideRule = GlideTestRule() 94 95 /* Setup dependencies for the UninstallModules for the test class. */ 96 @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule() 97 98 val testDispatcher = StandardTestDispatcher() 99 100 /* Overrides for ActivityModule */ 101 val testScope: TestScope = TestScope(testDispatcher) 102 @BindValue @Main val mainScope: CoroutineScope = testScope 103 @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope 104 105 /* Overrides for the ConcurrencyModule */ 106 @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher 107 @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher 108 109 @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver() 110 111 @Inject lateinit var selection: Lazy<Selection<Media>> 112 @Inject lateinit var featureManager: Lazy<FeatureManager> 113 @Inject lateinit var events: Lazy<Events> 114 @Inject override lateinit var configurationManager: Lazy<ConfigurationManager> 115 116 // Needed for UserMonitor 117 @Inject lateinit var mockContext: Context 118 @Mock lateinit var mockUserManager: UserManager 119 @Mock lateinit var mockPackageManager: PackageManager 120 121 @Before setupnull122 fun setup() { 123 MockitoAnnotations.initMocks(this) 124 hiltRule.inject() 125 setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager) 126 127 val testIntent = Intent(Intent.ACTION_GET_CONTENT) 128 configurationManager.get().setIntent(testIntent) 129 } 130 131 @Test testBrowseEnabledInConfigurationsnull132 fun testBrowseEnabledInConfigurations() { 133 assertWithMessage("BrowseFeature is enabled (ACTION_PICK_IMAGES)") 134 .that( 135 BrowseFeature.Registration.isEnabled( 136 TestPhotopickerConfiguration.build { 137 action(MediaStore.ACTION_PICK_IMAGES) 138 intent(Intent(MediaStore.ACTION_PICK_IMAGES)) 139 } 140 ) 141 ) 142 .isEqualTo(false) 143 144 assertWithMessage("BrowseFeature is not always enabled (ACTION_GET_CONTENT)") 145 .that( 146 BrowseFeature.Registration.isEnabled( 147 TestPhotopickerConfiguration.build { 148 action(Intent.ACTION_GET_CONTENT) 149 intent(Intent(Intent.ACTION_GET_CONTENT)) 150 } 151 ) 152 ) 153 .isEqualTo(true) 154 155 assertWithMessage("BrowseFeature is enabled (USER_SELECT_FOR_APP)") 156 .that( 157 BrowseFeature.Registration.isEnabled( 158 TestPhotopickerConfiguration.build { 159 action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) 160 intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) 161 callingPackage("com.example.test") 162 callingPackageUid(1234) 163 callingPackageLabel("test_app") 164 } 165 ) 166 ) 167 .isEqualTo(false) 168 169 assertWithMessage("BrowseFeature is enabled in Embedded configurations") 170 .that( 171 BrowseFeature.Registration.isEnabled( 172 TestPhotopickerConfiguration.build { 173 runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED) 174 } 175 ) 176 ) 177 .isEqualTo(false) 178 } 179 180 @Test testBrowseOverflowMenuItemIsDisplayednull181 fun testBrowseOverflowMenuItemIsDisplayed() = 182 testScope.runTest { 183 composeTestRule.setContent { 184 callPhotopickerMain( 185 featureManager = featureManager.get(), 186 selection = selection.get(), 187 events = events.get(), 188 ) 189 } 190 191 assertWithMessage("OverflowMenuFeature is not enabled") 192 .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java)) 193 .isTrue() 194 195 composeTestRule 196 .onNode( 197 hasContentDescription( 198 getTestableContext() 199 .getResources() 200 .getString(R.string.photopicker_overflow_menu_description) 201 ) 202 ) 203 .performClick() 204 205 composeTestRule 206 .onNode( 207 hasText( 208 getTestableContext() 209 .getResources() 210 .getString(R.string.photopicker_overflow_browse) 211 ) 212 ) 213 .assertIsDisplayed() 214 } 215 216 @Test testBrowseOverflowMenuItemDispatchedEventnull217 fun testBrowseOverflowMenuItemDispatchedEvent() = 218 testScope.runTest { 219 val eventDispatches = mutableListOf<Event>() 220 backgroundScope.launch { events.get().flow.toList(eventDispatches) } 221 222 composeTestRule.setContent { 223 callPhotopickerMain( 224 featureManager = featureManager.get(), 225 selection = selection.get(), 226 events = events.get(), 227 ) 228 } 229 230 assertWithMessage("OverflowMenuFeature is not enabled") 231 .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java)) 232 .isTrue() 233 234 composeTestRule 235 .onNode( 236 hasContentDescription( 237 getTestableContext() 238 .getResources() 239 .getString(R.string.photopicker_overflow_menu_description) 240 ) 241 ) 242 .performClick() 243 244 composeTestRule.waitForIdle() 245 246 composeTestRule 247 .onNode( 248 hasText( 249 getTestableContext() 250 .getResources() 251 .getString(R.string.photopicker_overflow_browse) 252 ) 253 ) 254 .performClick() 255 256 composeTestRule.waitForIdle() 257 advanceTimeBy(100) 258 259 assertWithMessage("Expected BrowseToDocumentsUI event to be dispatched") 260 .that(eventDispatches) 261 .contains(Event.BrowseToDocumentsUi(FeatureToken.BROWSE.token)) 262 } 263 } 264