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