1 /*
<lambda>null2 * Copyright 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 androidx.camera.compose
18
19 import android.content.Context
20 import androidx.camera.camera2.Camera2Config
21 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
22 import androidx.camera.core.Camera
23 import androidx.camera.core.CameraXConfig
24 import androidx.camera.core.Preview
25 import androidx.camera.core.SurfaceRequest
26 import androidx.camera.lifecycle.ProcessCameraProvider
27 import androidx.camera.testing.impl.CameraPipeConfigTestRule
28 import androidx.camera.testing.impl.CameraUtil
29 import androidx.camera.testing.impl.CameraUtil.PreTestCameraIdList
30 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
31 import androidx.camera.viewfinder.core.ImplementationMode
32 import androidx.compose.runtime.collectAsState
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.mutableStateOf
35 import androidx.compose.runtime.setValue
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.platform.testTag
38 import androidx.compose.ui.test.SemanticsMatcher
39 import androidx.compose.ui.test.assert
40 import androidx.compose.ui.test.assertIsDisplayed
41 import androidx.compose.ui.test.assertIsNotDisplayed
42 import androidx.compose.ui.test.junit4.createComposeRule
43 import androidx.compose.ui.test.onNodeWithTag
44 import androidx.concurrent.futures.await
45 import androidx.test.core.app.ApplicationProvider
46 import androidx.test.filters.LargeTest
47 import com.google.common.truth.Truth.assertThat
48 import kotlin.coroutines.CoroutineContext
49 import kotlin.coroutines.resume
50 import kotlin.time.Duration.Companion.seconds
51 import kotlinx.coroutines.CoroutineScope
52 import kotlinx.coroutines.DelicateCoroutinesApi
53 import kotlinx.coroutines.Dispatchers
54 import kotlinx.coroutines.flow.MutableStateFlow
55 import kotlinx.coroutines.flow.StateFlow
56 import kotlinx.coroutines.flow.asStateFlow
57 import kotlinx.coroutines.flow.filterNotNull
58 import kotlinx.coroutines.flow.first
59 import kotlinx.coroutines.flow.produceIn
60 import kotlinx.coroutines.flow.take
61 import kotlinx.coroutines.runBlocking
62 import kotlinx.coroutines.suspendCancellableCoroutine
63 import kotlinx.coroutines.withContext
64 import kotlinx.coroutines.withTimeout
65 import org.junit.Rule
66 import org.junit.Test
67 import org.junit.runner.RunWith
68 import org.junit.runners.Parameterized
69
70 @LargeTest
71 @RunWith(Parameterized::class)
72 class CameraXViewfinderTest(private val implName: String, private val cameraConfig: CameraXConfig) {
73 @get:Rule
74 val cameraPipeConfigTestRule =
75 CameraPipeConfigTestRule(
76 active = implName == CameraPipeConfig::class.simpleName,
77 )
78
79 @get:Rule
80 val useCamera =
81 CameraUtil.grantCameraPermissionAndPreTestAndPostTest(PreTestCameraIdList(cameraConfig))
82
83 @get:Rule val composeTest = createComposeRule()
84
85 @Test
86 fun viewfinderIsDisplayed_withValidSurfaceRequest() = runViewfinderTest {
87 composeTest.setContent {
88 val currentSurfaceRequest: SurfaceRequest? by surfaceRequests.collectAsState()
89 currentSurfaceRequest?.let { surfaceRequest ->
90 CameraXViewfinder(
91 surfaceRequest = surfaceRequest,
92 modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
93 )
94 }
95 }
96
97 // Start the camera
98 startCamera()
99
100 // Wait for first SurfaceRequest
101 surfaceRequests.filterNotNull().first()
102
103 composeTest.awaitIdle()
104
105 // CameraXViewfinder should now have a child Viewfinder
106 composeTest
107 .onNodeWithTag(CAMERAX_VIEWFINDER_TEST_TAG)
108 .assertIsDisplayed()
109 .assert(SemanticsMatcher.hasChild())
110 }
111
112 @OptIn(DelicateCoroutinesApi::class)
113 @Test
114 fun changingImplementation_sendsNewSurfaceRequest() = runViewfinderTest {
115 var implementationMode: ImplementationMode by mutableStateOf(ImplementationMode.EXTERNAL)
116 composeTest.setContent {
117 val currentSurfaceRequest: SurfaceRequest? by surfaceRequests.collectAsState()
118 currentSurfaceRequest?.let { surfaceRequest ->
119 CameraXViewfinder(
120 surfaceRequest = surfaceRequest,
121 implementationMode = implementationMode,
122 modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
123 )
124 }
125 }
126
127 // Collect expected number of SurfaceRequests for 2 mode changes
128 val surfaceRequestSequence = surfaceRequests.filterNotNull().take(3).produceIn(this)
129
130 // Start the camera
131 startCamera()
132
133 // Swap implementation modes twice to produce 3 SurfaceRequests
134 val allSurfaceRequests = buildList {
135 for (surfaceRequest in surfaceRequestSequence) {
136 add(surfaceRequest)
137 composeTest.awaitIdle()
138
139 if (!surfaceRequestSequence.isClosedForReceive) {
140 // Changing the implementation mode will invalidate the previous SurfaceRequest
141 // and cause Preview to send a new SurfaceRequest
142 implementationMode = implementationMode.swapMode()
143 composeTest.awaitIdle()
144 }
145 }
146 }
147
148 assertThat(allSurfaceRequests.size).isEqualTo(3)
149 assertThat(allSurfaceRequests).containsNoDuplicates()
150 }
151
152 @Test
153 fun cancelledSurfaceRequest_doesNotInstantiateViewfinder() = runViewfinderTest {
154 // Start the camera
155 startCamera()
156
157 // Wait for first SurfaceRequest
158 val surfaceRequest = surfaceRequests.filterNotNull().first()
159
160 // Reset surface provider to cause cancellation of the last SurfaceRequest
161 resetPreviewSurfaceProvider()
162
163 // Ensure the SurfaceRequest is cancelled
164 surfaceRequest.awaitCancellation()
165
166 // Pass on cancelled SurfaceRequest to CameraXViewfinder
167 composeTest.setContent {
168 CameraXViewfinder(
169 surfaceRequest = surfaceRequest,
170 modifier = Modifier.testTag(CAMERAX_VIEWFINDER_TEST_TAG)
171 )
172 }
173
174 composeTest.awaitIdle()
175
176 // Viewfinder should not be displayed since SurfaceRequest was cancelled
177 composeTest.onNodeWithTag(CAMERAX_VIEWFINDER_TEST_TAG).assertIsNotDisplayed()
178 }
179
180 companion object {
181 @JvmStatic
182 @Parameterized.Parameters(name = "{0}")
183 fun data() =
184 listOf(
185 arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
186 arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
187 )
188
189 private const val CAMERAX_VIEWFINDER_TEST_TAG = "CameraXViewfinderTestTag"
190 }
191
192 private inline fun runViewfinderTest(crossinline block: suspend PreviewTestScope.() -> Unit) =
193 runBlocking {
194 val context = ApplicationProvider.getApplicationContext<Context>()
195 val cameraProvider =
196 withTimeout(10.seconds) {
197 ProcessCameraProvider.configureInstance(cameraConfig)
198 ProcessCameraProvider.getInstance(context).await()
199 }
200
201 var fakeLifecycleOwner: FakeLifecycleOwner? = null
202 try {
203 val preview = Preview.Builder().build()
204 val surfaceRequests = MutableStateFlow<SurfaceRequest?>(null)
205 val resetPreviewSurfaceProvider =
206 suspend {
207 withContext(Dispatchers.Main) {
208 // Reset the surface provider to a new lambda that will continue to
209 // publish to surfaceRequests
210 preview.setSurfaceProvider { surfaceRequest ->
211 surfaceRequests.value = surfaceRequest
212 }
213 }
214 }
215 .also { it.invoke() }
216
217 val startCamera = suspend {
218 withContext(Dispatchers.Main) {
219 val lifecycleOwner =
220 FakeLifecycleOwner().apply {
221 startAndResume()
222 fakeLifecycleOwner = this
223 }
224
225 val firstAvailableCameraSelector =
226 cameraProvider.availableCameraInfos
227 .asSequence()
228 .map { it.cameraSelector }
229 .first()
230 cameraProvider.bindToLifecycle(
231 lifecycleOwner,
232 firstAvailableCameraSelector,
233 preview
234 )
235 }
236 }
237
238 with(
239 PreviewTestScope(
240 surfaceRequests = surfaceRequests.asStateFlow(),
241 resetPreviewSurfaceProvider = resetPreviewSurfaceProvider,
242 startCamera = startCamera,
243 coroutineContext = coroutineContext
244 )
245 ) {
246 block()
247 }
248 } finally {
249 fakeLifecycleOwner?.apply {
250 withContext(Dispatchers.Main) {
251 pauseAndStop()
252 destroy()
253 }
254 }
255 withTimeout(30.seconds) { cameraProvider.shutdownAsync().await() }
256 }
257 }
258
259 private data class PreviewTestScope(
260 val surfaceRequests: StateFlow<SurfaceRequest?>,
261 val resetPreviewSurfaceProvider: suspend () -> Unit,
262 val startCamera: suspend () -> Camera,
263 override val coroutineContext: CoroutineContext
264 ) : CoroutineScope
265 }
266
swapModenull267 private fun ImplementationMode.swapMode(): ImplementationMode {
268 return when (this) {
269 ImplementationMode.EXTERNAL -> ImplementationMode.EMBEDDED
270 ImplementationMode.EMBEDDED -> ImplementationMode.EXTERNAL
271 }
272 }
273
hasChildnull274 private fun SemanticsMatcher.Companion.hasChild() =
275 SemanticsMatcher("Has child") { node -> node.children.isNotEmpty() }
276
awaitCancellationnull277 private suspend fun SurfaceRequest.awaitCancellation(): Unit = suspendCancellableCoroutine { cont ->
278 addRequestCancellationListener(Runnable::run) { cont.resume(Unit) }
279 }
280