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
18 
19 import android.content.ContentResolver
20 import android.content.Context
21 import android.content.pm.PackageManager
22 import android.content.pm.UserProperties
23 import android.os.UserHandle
24 import android.os.UserManager
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.CompositionLocalProvider
27 import androidx.compose.runtime.getValue
28 import androidx.lifecycle.compose.collectAsStateWithLifecycle
29 import androidx.navigation.compose.ComposeNavigator
30 import androidx.navigation.compose.DialogNavigator
31 import androidx.navigation.testing.TestNavHostController
32 import androidx.test.platform.app.InstrumentationRegistry
33 import com.android.modules.utils.build.SdkLevel
34 import com.android.photopicker.R
35 import com.android.photopicker.core.PhotopickerMain
36 import com.android.photopicker.core.configuration.ConfigurationManager
37 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
38 import com.android.photopicker.core.events.Events
39 import com.android.photopicker.core.events.LocalEvents
40 import com.android.photopicker.core.features.FeatureManager
41 import com.android.photopicker.core.features.LocalFeatureManager
42 import com.android.photopicker.core.navigation.LocalNavController
43 import com.android.photopicker.core.selection.LocalSelection
44 import com.android.photopicker.core.selection.Selection
45 import com.android.photopicker.core.theme.PhotopickerTheme
46 import com.android.photopicker.data.model.Media
47 import com.android.photopicker.util.test.mockSystemService
48 import com.android.photopicker.util.test.whenever
49 import dagger.Lazy
50 import kotlinx.coroutines.flow.Flow
51 import kotlinx.coroutines.flow.flow
52 import org.mockito.Mockito.any
53 import org.mockito.Mockito.anyInt
54 import org.mockito.Mockito.anyString
55 
56 /**
57  * A base test class that includes some common utilities for starting a UI test with the Photopicker
58  * compose UI.
59  */
60 abstract class PhotopickerFeatureBaseTest {
61 
62     lateinit var navController: TestNavHostController
63 
64     // Hilt can't inject fields in the super class, so mark the field as abstract to force the
65     // implementer to provide.
66     abstract var configurationManager: Lazy<ConfigurationManager>
67 
68     /** A default implementation for retrieving a real context object for use during tests. */
getTestableContextnull69     protected fun getTestableContext(): Context {
70         return InstrumentationRegistry.getInstrumentation().getContext()
71     }
72 
73     /**
74      * Generates a suitable [TestNavHostController] which can be provided to the compose stack and
75      * allow tests to directly navigate.
76      */
createNavControllernull77     protected fun createNavController(): TestNavHostController {
78         navController = TestNavHostController(getTestableContext())
79         navController.navigatorProvider.addNavigator(ComposeNavigator())
80         navController.navigatorProvider.addNavigator(DialogNavigator())
81         return navController
82     }
83 
84     /** Generate a standard set of mocks that [UserMonitor] will need for test users. */
setupTestForUserMonitornull85     protected fun setupTestForUserMonitor(
86         mockContext: Context,
87         mockUserManager: UserManager,
88         contentResolver: ContentResolver,
89         mockPackageManager: PackageManager,
90     ) {
91         // Stub out UserManager with the mock
92         mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
93 
94         val resources = getTestableContext().getResources()
95 
96         if (SdkLevel.isAtLeastV()) {
97             whenever(mockUserManager.getUserBadge()) {
98                 resources.getDrawable(R.drawable.android, /* theme= */ null)
99             }
100             whenever(mockUserManager.getProfileLabel())
101                 .thenReturn(
102                     resources.getString(R.string.photopicker_profile_primary_label),
103                     resources.getString(R.string.photopicker_profile_managed_label),
104                     resources.getString(R.string.photopicker_profile_unknown_label),
105                 )
106             // Return default [UserProperties] for all [UserHandle]
107             whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
108                 UserProperties.Builder().build()
109             }
110         }
111 
112         // Stubs for UserMonitor to acquire contentResolver for each User.
113         whenever(mockContext.contentResolver) { contentResolver }
114         whenever(mockContext.packageManager) { mockPackageManager }
115         whenever(mockContext.packageName) { "com.android.photopicker" }
116 
117         // Recursively return the same mockContext for all user packages to keep the stubbing
118         // simple.
119         whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
120             mockContext
121         }
122         whenever(
123             mockContext.createPackageContextAsUser(
124                 anyString(),
125                 anyInt(),
126                 any(UserHandle::class.java),
127             )
128         ) {
129             mockContext
130         }
131     }
132 
133     /**
134      * A helper method that calls into the [PhotopickerMain] composable in the UI stack and provides
135      * the correct [CompositionLocalProvider]s required to bootstrap the UI.
136      */
137     @Composable
callPhotopickerMainnull138     protected fun callPhotopickerMain(
139         featureManager: FeatureManager,
140         selection: Selection<Media>,
141         events: Events,
142         navController: TestNavHostController = createNavController(),
143         disruptiveDataFlow: Flow<Int> = flow { emit(0) },
144     ) {
145         val photopickerConfiguration by
146             configurationManager.get().configuration.collectAsStateWithLifecycle()
147 
148         CompositionLocalProvider(
149             LocalFeatureManager provides featureManager,
150             LocalSelection provides selection,
151             LocalPhotopickerConfiguration provides photopickerConfiguration,
152             LocalNavController provides navController,
153             LocalEvents provides events,
<lambda>null154         ) {
155             PhotopickerTheme(config = photopickerConfiguration) {
156                 PhotopickerMain(disruptiveDataNotification = disruptiveDataFlow)
157             }
158         }
159     }
160 }
161