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