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